Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
5d96aaf7f1
143 changed files with 1433 additions and 1076 deletions
4
Gemfile
4
Gemfile
|
@ -13,11 +13,11 @@ gem 'pg', '~> 0.20'
|
||||||
gem 'pghero', '~> 1.7'
|
gem 'pghero', '~> 1.7'
|
||||||
gem 'dotenv-rails', '~> 2.2'
|
gem 'dotenv-rails', '~> 2.2'
|
||||||
|
|
||||||
gem 'aws-sdk', '~> 2.10', require: false
|
gem 'aws-sdk-s3', '~> 1.8', require: false
|
||||||
gem 'fog-core', '~> 1.45'
|
gem 'fog-core', '~> 1.45'
|
||||||
gem 'fog-local', '~> 0.4', require: false
|
gem 'fog-local', '~> 0.4', require: false
|
||||||
gem 'fog-openstack', '~> 0.1', require: false
|
gem 'fog-openstack', '~> 0.1', require: false
|
||||||
gem 'paperclip', '~> 5.1'
|
gem 'paperclip', '~> 6.0'
|
||||||
gem 'paperclip-av-transcoder', '~> 0.6'
|
gem 'paperclip-av-transcoder', '~> 0.6'
|
||||||
gem 'streamio-ffmpeg', '~> 3.0'
|
gem 'streamio-ffmpeg', '~> 3.0'
|
||||||
|
|
||||||
|
|
29
Gemfile.lock
29
Gemfile.lock
|
@ -57,13 +57,18 @@ GEM
|
||||||
encryptor (~> 3.0.0)
|
encryptor (~> 3.0.0)
|
||||||
av (0.9.0)
|
av (0.9.0)
|
||||||
cocaine (~> 0.5.3)
|
cocaine (~> 0.5.3)
|
||||||
aws-sdk (2.10.100)
|
aws-partitions (1.70.0)
|
||||||
aws-sdk-resources (= 2.10.100)
|
aws-sdk-core (3.17.0)
|
||||||
aws-sdk-core (2.10.100)
|
aws-partitions (~> 1.0)
|
||||||
aws-sigv4 (~> 1.0)
|
aws-sigv4 (~> 1.0)
|
||||||
jmespath (~> 1.0)
|
jmespath (~> 1.0)
|
||||||
aws-sdk-resources (2.10.100)
|
aws-sdk-kms (1.5.0)
|
||||||
aws-sdk-core (= 2.10.100)
|
aws-sdk-core (~> 3)
|
||||||
|
aws-sigv4 (~> 1.0)
|
||||||
|
aws-sdk-s3 (1.8.2)
|
||||||
|
aws-sdk-core (~> 3)
|
||||||
|
aws-sdk-kms (~> 1)
|
||||||
|
aws-sigv4 (~> 1.0)
|
||||||
aws-sigv4 (1.0.2)
|
aws-sigv4 (1.0.2)
|
||||||
bcrypt (3.1.11)
|
bcrypt (3.1.11)
|
||||||
better_errors (2.4.0)
|
better_errors (2.4.0)
|
||||||
|
@ -236,7 +241,7 @@ GEM
|
||||||
httplog (0.99.7)
|
httplog (0.99.7)
|
||||||
colorize
|
colorize
|
||||||
rack
|
rack
|
||||||
i18n (0.9.3)
|
i18n (0.9.5)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
i18n-tasks (0.9.19)
|
i18n-tasks (0.9.19)
|
||||||
activesupport (>= 4.0.2)
|
activesupport (>= 4.0.2)
|
||||||
|
@ -342,12 +347,12 @@ GEM
|
||||||
http (~> 3.0)
|
http (~> 3.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
ox (2.8.2)
|
ox (2.8.2)
|
||||||
paperclip (5.2.1)
|
paperclip (6.0.0)
|
||||||
activemodel (>= 4.2.0)
|
activemodel (>= 4.2.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
cocaine (~> 0.5.5)
|
|
||||||
mime-types
|
mime-types
|
||||||
mimemagic (~> 0.3.0)
|
mimemagic (~> 0.3.0)
|
||||||
|
terrapin (~> 0.6.0)
|
||||||
paperclip-av-transcoder (0.6.4)
|
paperclip-av-transcoder (0.6.4)
|
||||||
av (~> 0.9.0)
|
av (~> 0.9.0)
|
||||||
paperclip (>= 2.5.2)
|
paperclip (>= 2.5.2)
|
||||||
|
@ -552,6 +557,8 @@ GEM
|
||||||
temple (0.8.0)
|
temple (0.8.0)
|
||||||
terminal-table (1.8.0)
|
terminal-table (1.8.0)
|
||||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||||
|
terrapin (0.6.0)
|
||||||
|
climate_control (>= 0.0.3, < 1.0)
|
||||||
thor (0.20.0)
|
thor (0.20.0)
|
||||||
thread (0.2.2)
|
thread (0.2.2)
|
||||||
thread_safe (0.3.6)
|
thread_safe (0.3.6)
|
||||||
|
@ -575,7 +582,7 @@ GEM
|
||||||
tty-screen (0.6.4)
|
tty-screen (0.6.4)
|
||||||
twitter-text (1.14.7)
|
twitter-text (1.14.7)
|
||||||
unf (~> 0.1.0)
|
unf (~> 0.1.0)
|
||||||
tzinfo (1.2.4)
|
tzinfo (1.2.5)
|
||||||
thread_safe (~> 0.1)
|
thread_safe (~> 0.1)
|
||||||
tzinfo-data (1.2017.3)
|
tzinfo-data (1.2017.3)
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
|
@ -612,7 +619,7 @@ DEPENDENCIES
|
||||||
active_record_query_trace (~> 1.5)
|
active_record_query_trace (~> 1.5)
|
||||||
addressable (~> 2.5)
|
addressable (~> 2.5)
|
||||||
annotate (~> 2.7)
|
annotate (~> 2.7)
|
||||||
aws-sdk (~> 2.10)
|
aws-sdk-s3 (~> 1.8)
|
||||||
better_errors (~> 2.4)
|
better_errors (~> 2.4)
|
||||||
binding_of_caller (~> 0.7)
|
binding_of_caller (~> 0.7)
|
||||||
bootsnap
|
bootsnap
|
||||||
|
@ -671,7 +678,7 @@ DEPENDENCIES
|
||||||
omniauth-saml (~> 1.10)
|
omniauth-saml (~> 1.10)
|
||||||
ostatus2 (~> 2.0)
|
ostatus2 (~> 2.0)
|
||||||
ox (~> 2.8)
|
ox (~> 2.8)
|
||||||
paperclip (~> 5.1)
|
paperclip (~> 6.0)
|
||||||
paperclip-av-transcoder (~> 0.6)
|
paperclip-av-transcoder (~> 0.6)
|
||||||
parallel_tests (~> 2.17)
|
parallel_tests (~> 2.17)
|
||||||
pg (~> 0.20)
|
pg (~> 0.20)
|
||||||
|
|
|
@ -60,9 +60,9 @@ module JsonLdHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_resource_without_id_validation(uri)
|
def fetch_resource_without_id_validation(uri)
|
||||||
response = build_request(uri).perform
|
build_request(uri).perform do |response|
|
||||||
return if response.code != 200
|
response.code == 200 ? body_to_json(response.body_with_limit) : nil
|
||||||
body_to_json(response.to_s)
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def body_to_json(body)
|
def body_to_json(body)
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
|
import asyncDB from '../db/async';
|
||||||
|
import { importAccount, importFetchedAccount, importFetchedAccounts } from './importer';
|
||||||
|
|
||||||
export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
|
export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
|
||||||
export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
|
export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
|
||||||
|
@ -64,6 +66,24 @@ 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_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
|
||||||
export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
|
export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
|
||||||
|
|
||||||
|
function getFromDB(dispatch, getState, index, id) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = index.get(id);
|
||||||
|
|
||||||
|
request.onerror = reject;
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
if (!request.result) {
|
||||||
|
reject();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(importAccount(request.result));
|
||||||
|
resolve(request.result.moved && getFromDB(dispatch, getState, index, request.result.moved));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchAccount(id) {
|
export function fetchAccount(id) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(fetchRelationships([id]));
|
dispatch(fetchRelationships([id]));
|
||||||
|
@ -74,9 +94,16 @@ export function fetchAccount(id) {
|
||||||
|
|
||||||
dispatch(fetchAccountRequest(id));
|
dispatch(fetchAccountRequest(id));
|
||||||
|
|
||||||
api(getState).get(`/api/v1/accounts/${id}`).then(response => {
|
asyncDB.then(db => getFromDB(
|
||||||
dispatch(fetchAccountSuccess(response.data));
|
dispatch,
|
||||||
}).catch(error => {
|
getState,
|
||||||
|
db.transaction('accounts', 'read').objectStore('accounts').index('id'),
|
||||||
|
id
|
||||||
|
)).catch(() => api(getState).get(`/api/v1/accounts/${id}`).then(response => {
|
||||||
|
dispatch(importFetchedAccount(response.data));
|
||||||
|
})).then(() => {
|
||||||
|
dispatch(fetchAccountSuccess());
|
||||||
|
}, error => {
|
||||||
dispatch(fetchAccountFail(id, error));
|
dispatch(fetchAccountFail(id, error));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -89,10 +116,9 @@ export function fetchAccountRequest(id) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function fetchAccountSuccess(account) {
|
export function fetchAccountSuccess() {
|
||||||
return {
|
return {
|
||||||
type: ACCOUNT_FETCH_SUCCESS,
|
type: ACCOUNT_FETCH_SUCCESS,
|
||||||
account,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -319,6 +345,7 @@ export function fetchFollowers(id) {
|
||||||
api(getState).get(`/api/v1/accounts/${id}/followers`).then(response => {
|
api(getState).get(`/api/v1/accounts/${id}/followers`).then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
dispatch(fetchFollowersSuccess(id, response.data, next ? next.uri : null));
|
dispatch(fetchFollowersSuccess(id, response.data, next ? next.uri : null));
|
||||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
@ -364,6 +391,7 @@ export function expandFollowers(id) {
|
||||||
api(getState).get(url).then(response => {
|
api(getState).get(url).then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
dispatch(expandFollowersSuccess(id, response.data, next ? next.uri : null));
|
dispatch(expandFollowersSuccess(id, response.data, next ? next.uri : null));
|
||||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
@ -403,6 +431,7 @@ export function fetchFollowing(id) {
|
||||||
api(getState).get(`/api/v1/accounts/${id}/following`).then(response => {
|
api(getState).get(`/api/v1/accounts/${id}/following`).then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
dispatch(fetchFollowingSuccess(id, response.data, next ? next.uri : null));
|
dispatch(fetchFollowingSuccess(id, response.data, next ? next.uri : null));
|
||||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
@ -448,6 +477,7 @@ export function expandFollowing(id) {
|
||||||
api(getState).get(url).then(response => {
|
api(getState).get(url).then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
dispatch(expandFollowingSuccess(id, response.data, next ? next.uri : null));
|
dispatch(expandFollowingSuccess(id, response.data, next ? next.uri : null));
|
||||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
@ -529,6 +559,7 @@ export function fetchFollowRequests() {
|
||||||
|
|
||||||
api(getState).get('/api/v1/follow_requests').then(response => {
|
api(getState).get('/api/v1/follow_requests').then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
dispatch(fetchFollowRequestsSuccess(response.data, next ? next.uri : null));
|
dispatch(fetchFollowRequestsSuccess(response.data, next ? next.uri : null));
|
||||||
}).catch(error => dispatch(fetchFollowRequestsFail(error)));
|
}).catch(error => dispatch(fetchFollowRequestsFail(error)));
|
||||||
};
|
};
|
||||||
|
@ -567,6 +598,7 @@ export function expandFollowRequests() {
|
||||||
|
|
||||||
api(getState).get(url).then(response => {
|
api(getState).get(url).then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
dispatch(expandFollowRequestsSuccess(response.data, next ? next.uri : null));
|
dispatch(expandFollowRequestsSuccess(response.data, next ? next.uri : null));
|
||||||
}).catch(error => dispatch(expandFollowRequestsFail(error)));
|
}).catch(error => dispatch(expandFollowRequestsFail(error)));
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
import { fetchRelationships } from './accounts';
|
import { fetchRelationships } from './accounts';
|
||||||
|
import { importFetchedAccounts } from './importer';
|
||||||
|
|
||||||
export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST';
|
export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST';
|
||||||
export const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS';
|
export const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS';
|
||||||
|
@ -15,6 +16,7 @@ export function fetchBlocks() {
|
||||||
|
|
||||||
api(getState).get('/api/v1/blocks').then(response => {
|
api(getState).get('/api/v1/blocks').then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
dispatch(fetchBlocksSuccess(response.data, next ? next.uri : null));
|
dispatch(fetchBlocksSuccess(response.data, next ? next.uri : null));
|
||||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||||
}).catch(error => dispatch(fetchBlocksFail(error)));
|
}).catch(error => dispatch(fetchBlocksFail(error)));
|
||||||
|
@ -54,6 +56,7 @@ export function expandBlocks() {
|
||||||
|
|
||||||
api(getState).get(url).then(response => {
|
api(getState).get(url).then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
dispatch(expandBlocksSuccess(response.data, next ? next.uri : null));
|
dispatch(expandBlocksSuccess(response.data, next ? next.uri : null));
|
||||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||||
}).catch(error => dispatch(expandBlocksFail(error)));
|
}).catch(error => dispatch(expandBlocksFail(error)));
|
||||||
|
|
|
@ -4,13 +4,8 @@ import { throttle } from 'lodash';
|
||||||
import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
|
import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
|
||||||
import { tagHistory } from '../settings';
|
import { tagHistory } from '../settings';
|
||||||
import { useEmoji } from './emojis';
|
import { useEmoji } from './emojis';
|
||||||
|
import { importFetchedAccounts } from './importer';
|
||||||
import {
|
import { updateTimeline } from './timelines';
|
||||||
updateTimeline,
|
|
||||||
refreshHomeTimeline,
|
|
||||||
refreshCommunityTimeline,
|
|
||||||
refreshPublicTimeline,
|
|
||||||
} from './timelines';
|
|
||||||
|
|
||||||
let cancelFetchComposeSuggestionsAccounts;
|
let cancelFetchComposeSuggestionsAccounts;
|
||||||
|
|
||||||
|
@ -124,19 +119,17 @@ export function submitCompose() {
|
||||||
|
|
||||||
// To make the app more responsive, immediately get the status into the columns
|
// To make the app more responsive, immediately get the status into the columns
|
||||||
|
|
||||||
const insertOrRefresh = (timelineId, refreshAction) => {
|
const insertIfOnline = (timelineId) => {
|
||||||
if (getState().getIn(['timelines', timelineId, 'online'])) {
|
if (getState().getIn(['timelines', timelineId, 'items', 0]) !== null) {
|
||||||
dispatch(updateTimeline(timelineId, { ...response.data }));
|
dispatch(updateTimeline(timelineId, { ...response.data }));
|
||||||
} else if (getState().getIn(['timelines', timelineId, 'loaded'])) {
|
|
||||||
dispatch(refreshAction());
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
insertOrRefresh('home', refreshHomeTimeline);
|
insertIfOnline('home');
|
||||||
|
|
||||||
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
|
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
|
||||||
insertOrRefresh('community', refreshCommunityTimeline);
|
insertIfOnline('community');
|
||||||
insertOrRefresh('public', refreshPublicTimeline);
|
insertIfOnline('public');
|
||||||
}
|
}
|
||||||
}).catch(function (error) {
|
}).catch(function (error) {
|
||||||
dispatch(submitComposeFail(error));
|
dispatch(submitComposeFail(error));
|
||||||
|
@ -282,6 +275,7 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) =>
|
||||||
limit: 4,
|
limit: 4,
|
||||||
},
|
},
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
dispatch(readyComposeSuggestionsAccounts(token, response.data));
|
dispatch(readyComposeSuggestionsAccounts(token, response.data));
|
||||||
});
|
});
|
||||||
}, 200, { leading: true, trailing: true });
|
}, 200, { leading: true, trailing: true });
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
|
import { importFetchedStatuses } from './importer';
|
||||||
|
|
||||||
export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST';
|
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_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS';
|
||||||
|
@ -18,6 +19,7 @@ export function fetchFavouritedStatuses() {
|
||||||
|
|
||||||
api(getState).get('/api/v1/favourites').then(response => {
|
api(getState).get('/api/v1/favourites').then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(importFetchedStatuses(response.data));
|
||||||
dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null));
|
dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchFavouritedStatusesFail(error));
|
dispatch(fetchFavouritedStatusesFail(error));
|
||||||
|
@ -58,6 +60,7 @@ export function expandFavouritedStatuses() {
|
||||||
|
|
||||||
api(getState).get(url).then(response => {
|
api(getState).get(url).then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(importFetchedStatuses(response.data));
|
||||||
dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null));
|
dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(expandFavouritedStatusesFail(error));
|
dispatch(expandFavouritedStatusesFail(error));
|
||||||
|
|
76
app/javascript/mastodon/actions/importer/index.js
Normal file
76
app/javascript/mastodon/actions/importer/index.js
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import { putAccounts, putStatuses } from '../../db/modifier';
|
||||||
|
import { normalizeAccount, normalizeStatus } from './normalizer';
|
||||||
|
|
||||||
|
export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT';
|
||||||
|
export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT';
|
||||||
|
export const STATUS_IMPORT = 'STATUS_IMPORT';
|
||||||
|
export const STATUSES_IMPORT = 'STATUSES_IMPORT';
|
||||||
|
|
||||||
|
function pushUnique(array, object) {
|
||||||
|
if (array.every(element => element.id !== object.id)) {
|
||||||
|
array.push(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function importAccount(account) {
|
||||||
|
return { type: ACCOUNT_IMPORT, account };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function importAccounts(accounts) {
|
||||||
|
return { type: ACCOUNTS_IMPORT, accounts };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function importStatus(status) {
|
||||||
|
return { type: STATUS_IMPORT, status };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function importStatuses(statuses) {
|
||||||
|
return { type: STATUSES_IMPORT, statuses };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function importFetchedAccount(account) {
|
||||||
|
return importFetchedAccounts([account]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function importFetchedAccounts(accounts) {
|
||||||
|
const normalAccounts = [];
|
||||||
|
|
||||||
|
function processAccount(account) {
|
||||||
|
pushUnique(normalAccounts, normalizeAccount(account));
|
||||||
|
|
||||||
|
if (account.moved) {
|
||||||
|
processAccount(account.moved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts.forEach(processAccount);
|
||||||
|
putAccounts(normalAccounts);
|
||||||
|
|
||||||
|
return importAccounts(normalAccounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function importFetchedStatus(status) {
|
||||||
|
return importFetchedStatuses([status]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function importFetchedStatuses(statuses) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const accounts = [];
|
||||||
|
const normalStatuses = [];
|
||||||
|
|
||||||
|
function processStatus(status) {
|
||||||
|
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id])));
|
||||||
|
pushUnique(accounts, status.account);
|
||||||
|
|
||||||
|
if (status.reblog && status.reblog.id) {
|
||||||
|
processStatus(status.reblog);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
statuses.forEach(processStatus);
|
||||||
|
putStatuses(normalStatuses);
|
||||||
|
|
||||||
|
dispatch(importFetchedAccounts(accounts));
|
||||||
|
dispatch(importStatuses(normalStatuses));
|
||||||
|
};
|
||||||
|
}
|
50
app/javascript/mastodon/actions/importer/normalizer.js
Normal file
50
app/javascript/mastodon/actions/importer/normalizer.js
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
|
import emojify from '../../features/emoji/emoji';
|
||||||
|
|
||||||
|
const domParser = new DOMParser();
|
||||||
|
|
||||||
|
export function normalizeAccount(account) {
|
||||||
|
account = { ...account };
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (account.moved) {
|
||||||
|
account.moved = account.moved.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeStatus(status, normalOldStatus) {
|
||||||
|
const normalStatus = { ...status };
|
||||||
|
normalStatus.account = status.account.id;
|
||||||
|
|
||||||
|
if (status.reblog && status.reblog.id) {
|
||||||
|
normalStatus.reblog = status.reblog.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only calculate these values when status first encountered
|
||||||
|
// Otherwise keep the ones already in the reducer
|
||||||
|
if (normalOldStatus) {
|
||||||
|
normalStatus.search_index = normalOldStatus.get('search_index');
|
||||||
|
normalStatus.contentHtml = normalOldStatus.get('contentHtml');
|
||||||
|
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
|
||||||
|
normalStatus.hidden = normalOldStatus.get('hidden');
|
||||||
|
} else {
|
||||||
|
const searchContent = [status.spoiler_text, status.content].join('\n\n').replace(/<br\s*\/?>/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);
|
||||||
|
normalStatus.hidden = normalStatus.sensitive;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalStatus;
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
||||||
|
|
||||||
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
|
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
|
||||||
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
|
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
|
||||||
|
@ -39,7 +40,8 @@ export function reblog(status) {
|
||||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`).then(function (response) {
|
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
|
// 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
|
// interested in how the original is modified, hence passing it skipping the wrapper
|
||||||
dispatch(reblogSuccess(status, response.data.reblog));
|
dispatch(importFetchedStatus(response.data.reblog));
|
||||||
|
dispatch(reblogSuccess(status));
|
||||||
}).catch(function (error) {
|
}).catch(function (error) {
|
||||||
dispatch(reblogFail(status, error));
|
dispatch(reblogFail(status, error));
|
||||||
});
|
});
|
||||||
|
@ -51,7 +53,8 @@ export function unreblog(status) {
|
||||||
dispatch(unreblogRequest(status));
|
dispatch(unreblogRequest(status));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(response => {
|
api(getState).post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(response => {
|
||||||
dispatch(unreblogSuccess(status, response.data));
|
dispatch(importFetchedStatus(response.data));
|
||||||
|
dispatch(unreblogSuccess(status));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(unreblogFail(status, error));
|
dispatch(unreblogFail(status, error));
|
||||||
});
|
});
|
||||||
|
@ -66,11 +69,10 @@ export function reblogRequest(status) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function reblogSuccess(status, response) {
|
export function reblogSuccess(status) {
|
||||||
return {
|
return {
|
||||||
type: REBLOG_SUCCESS,
|
type: REBLOG_SUCCESS,
|
||||||
status: status,
|
status: status,
|
||||||
response: response,
|
|
||||||
skipLoading: true,
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -92,11 +94,10 @@ export function unreblogRequest(status) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function unreblogSuccess(status, response) {
|
export function unreblogSuccess(status) {
|
||||||
return {
|
return {
|
||||||
type: UNREBLOG_SUCCESS,
|
type: UNREBLOG_SUCCESS,
|
||||||
status: status,
|
status: status,
|
||||||
response: response,
|
|
||||||
skipLoading: true,
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -115,7 +116,8 @@ export function favourite(status) {
|
||||||
dispatch(favouriteRequest(status));
|
dispatch(favouriteRequest(status));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/favourite`).then(function (response) {
|
api(getState).post(`/api/v1/statuses/${status.get('id')}/favourite`).then(function (response) {
|
||||||
dispatch(favouriteSuccess(status, response.data));
|
dispatch(importFetchedStatus(response.data));
|
||||||
|
dispatch(favouriteSuccess(status));
|
||||||
}).catch(function (error) {
|
}).catch(function (error) {
|
||||||
dispatch(favouriteFail(status, error));
|
dispatch(favouriteFail(status, error));
|
||||||
});
|
});
|
||||||
|
@ -127,7 +129,8 @@ export function unfavourite(status) {
|
||||||
dispatch(unfavouriteRequest(status));
|
dispatch(unfavouriteRequest(status));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(response => {
|
api(getState).post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(response => {
|
||||||
dispatch(unfavouriteSuccess(status, response.data));
|
dispatch(importFetchedStatus(response.data));
|
||||||
|
dispatch(unfavouriteSuccess(status));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(unfavouriteFail(status, error));
|
dispatch(unfavouriteFail(status, error));
|
||||||
});
|
});
|
||||||
|
@ -142,11 +145,10 @@ export function favouriteRequest(status) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function favouriteSuccess(status, response) {
|
export function favouriteSuccess(status) {
|
||||||
return {
|
return {
|
||||||
type: FAVOURITE_SUCCESS,
|
type: FAVOURITE_SUCCESS,
|
||||||
status: status,
|
status: status,
|
||||||
response: response,
|
|
||||||
skipLoading: true,
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -168,11 +170,10 @@ export function unfavouriteRequest(status) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function unfavouriteSuccess(status, response) {
|
export function unfavouriteSuccess(status) {
|
||||||
return {
|
return {
|
||||||
type: UNFAVOURITE_SUCCESS,
|
type: UNFAVOURITE_SUCCESS,
|
||||||
status: status,
|
status: status,
|
||||||
response: response,
|
|
||||||
skipLoading: true,
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -191,6 +192,7 @@ export function fetchReblogs(id) {
|
||||||
dispatch(fetchReblogsRequest(id));
|
dispatch(fetchReblogsRequest(id));
|
||||||
|
|
||||||
api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
|
api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
dispatch(fetchReblogsSuccess(id, response.data));
|
dispatch(fetchReblogsSuccess(id, response.data));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchReblogsFail(id, error));
|
dispatch(fetchReblogsFail(id, error));
|
||||||
|
@ -225,6 +227,7 @@ export function fetchFavourites(id) {
|
||||||
dispatch(fetchFavouritesRequest(id));
|
dispatch(fetchFavouritesRequest(id));
|
||||||
|
|
||||||
api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
|
api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
dispatch(fetchFavouritesSuccess(id, response.data));
|
dispatch(fetchFavouritesSuccess(id, response.data));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchFavouritesFail(id, error));
|
dispatch(fetchFavouritesFail(id, error));
|
||||||
|
@ -259,7 +262,8 @@ export function pin(status) {
|
||||||
dispatch(pinRequest(status));
|
dispatch(pinRequest(status));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => {
|
api(getState).post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => {
|
||||||
dispatch(pinSuccess(status, response.data));
|
dispatch(importFetchedStatus(response.data));
|
||||||
|
dispatch(pinSuccess(status));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(pinFail(status, error));
|
dispatch(pinFail(status, error));
|
||||||
});
|
});
|
||||||
|
@ -274,11 +278,10 @@ export function pinRequest(status) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function pinSuccess(status, response) {
|
export function pinSuccess(status) {
|
||||||
return {
|
return {
|
||||||
type: PIN_SUCCESS,
|
type: PIN_SUCCESS,
|
||||||
status,
|
status,
|
||||||
response,
|
|
||||||
skipLoading: true,
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -297,7 +300,8 @@ export function unpin (status) {
|
||||||
dispatch(unpinRequest(status));
|
dispatch(unpinRequest(status));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => {
|
api(getState).post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => {
|
||||||
dispatch(unpinSuccess(status, response.data));
|
dispatch(importFetchedStatus(response.data));
|
||||||
|
dispatch(unpinSuccess(status));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(unpinFail(status, error));
|
dispatch(unpinFail(status, error));
|
||||||
});
|
});
|
||||||
|
@ -312,11 +316,10 @@ export function unpinRequest(status) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function unpinSuccess(status, response) {
|
export function unpinSuccess(status) {
|
||||||
return {
|
return {
|
||||||
type: UNPIN_SUCCESS,
|
type: UNPIN_SUCCESS,
|
||||||
status,
|
status,
|
||||||
response,
|
|
||||||
skipLoading: true,
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
import { importFetchedAccounts } from './importer';
|
||||||
|
|
||||||
export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
|
export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
|
||||||
export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
|
export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
|
||||||
|
@ -200,9 +201,10 @@ export const deleteListFail = (id, error) => ({
|
||||||
export const fetchListAccounts = listId => (dispatch, getState) => {
|
export const fetchListAccounts = listId => (dispatch, getState) => {
|
||||||
dispatch(fetchListAccountsRequest(listId));
|
dispatch(fetchListAccountsRequest(listId));
|
||||||
|
|
||||||
api(getState).get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } })
|
api(getState).get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } }).then(({ data }) => {
|
||||||
.then(({ data }) => dispatch(fetchListAccountsSuccess(listId, data)))
|
dispatch(importFetchedAccounts(data));
|
||||||
.catch(err => dispatch(fetchListAccountsFail(listId, err)));
|
dispatch(fetchListAccountsSuccess(listId, data));
|
||||||
|
}).catch(err => dispatch(fetchListAccountsFail(listId, err)));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchListAccountsRequest = id => ({
|
export const fetchListAccountsRequest = id => ({
|
||||||
|
@ -231,8 +233,10 @@ export const fetchListSuggestions = q => (dispatch, getState) => {
|
||||||
following: true,
|
following: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
api(getState).get('/api/v1/accounts/search', { params })
|
api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => {
|
||||||
.then(({ data }) => dispatch(fetchListSuggestionsReady(q, data)));
|
dispatch(importFetchedAccounts(data));
|
||||||
|
dispatch(fetchListSuggestionsReady(q, data));
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchListSuggestionsReady = (query, accounts) => ({
|
export const fetchListSuggestionsReady = (query, accounts) => ({
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
import { fetchRelationships } from './accounts';
|
import { fetchRelationships } from './accounts';
|
||||||
|
import { importFetchedAccounts } from './importer';
|
||||||
import { openModal } from './modal';
|
import { openModal } from './modal';
|
||||||
|
|
||||||
export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
|
export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
|
||||||
|
@ -19,6 +20,7 @@ export function fetchMutes() {
|
||||||
|
|
||||||
api(getState).get('/api/v1/mutes').then(response => {
|
api(getState).get('/api/v1/mutes').then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
dispatch(fetchMutesSuccess(response.data, next ? next.uri : null));
|
dispatch(fetchMutesSuccess(response.data, next ? next.uri : null));
|
||||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||||
}).catch(error => dispatch(fetchMutesFail(error)));
|
}).catch(error => dispatch(fetchMutesFail(error)));
|
||||||
|
@ -58,6 +60,7 @@ export function expandMutes() {
|
||||||
|
|
||||||
api(getState).get(url).then(response => {
|
api(getState).get(url).then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
dispatch(expandMutesSuccess(response.data, next ? next.uri : null));
|
dispatch(expandMutesSuccess(response.data, next ? next.uri : null));
|
||||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||||
}).catch(error => dispatch(expandMutesFail(error)));
|
}).catch(error => dispatch(expandMutesFail(error)));
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
import { List as ImmutableList } from 'immutable';
|
|
||||||
import IntlMessageFormat from 'intl-messageformat';
|
import IntlMessageFormat from 'intl-messageformat';
|
||||||
import { fetchRelationships } from './accounts';
|
import { fetchRelationships } from './accounts';
|
||||||
|
import {
|
||||||
|
importFetchedAccount,
|
||||||
|
importFetchedAccounts,
|
||||||
|
importFetchedStatus,
|
||||||
|
importFetchedStatuses,
|
||||||
|
} from './importer';
|
||||||
import { defineMessages } from 'react-intl';
|
import { defineMessages } from 'react-intl';
|
||||||
|
|
||||||
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
||||||
|
|
||||||
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_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
|
||||||
export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
|
export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
|
||||||
export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL';
|
export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL';
|
||||||
|
@ -41,11 +42,12 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
||||||
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
|
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
|
||||||
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
|
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
|
||||||
|
|
||||||
|
dispatch(importFetchedAccount(notification.account));
|
||||||
|
dispatch(importFetchedStatus(notification.status));
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: NOTIFICATIONS_UPDATE,
|
type: NOTIFICATIONS_UPDATE,
|
||||||
notification,
|
notification,
|
||||||
account: notification.account,
|
|
||||||
status: notification.status,
|
|
||||||
meta: playSound ? { sound: 'boop' } : undefined,
|
meta: playSound ? { sound: 'boop' } : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -67,73 +69,14 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
||||||
|
|
||||||
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
|
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
|
||||||
|
|
||||||
export function refreshNotifications() {
|
export function expandNotifications({ maxId } = {}) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const params = {};
|
if (getState().getIn(['notifications', 'isLoading'])) {
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
max_id: items.last().get('id'),
|
max_id: maxId,
|
||||||
limit: 20,
|
|
||||||
exclude_types: excludeTypesFromSettings(getState()),
|
exclude_types: excludeTypesFromSettings(getState()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -141,6 +84,10 @@ export function expandNotifications() {
|
||||||
|
|
||||||
api(getState).get('/api/v1/notifications', { params }).then(response => {
|
api(getState).get('/api/v1/notifications', { params }).then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
|
||||||
|
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
|
||||||
|
|
||||||
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null));
|
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null));
|
||||||
fetchRelatedRelationships(dispatch, response.data);
|
fetchRelatedRelationships(dispatch, response.data);
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
@ -159,8 +106,6 @@ export function expandNotificationsSuccess(notifications, next) {
|
||||||
return {
|
return {
|
||||||
type: NOTIFICATIONS_EXPAND_SUCCESS,
|
type: NOTIFICATIONS_EXPAND_SUCCESS,
|
||||||
notifications,
|
notifications,
|
||||||
accounts: notifications.map(item => item.account),
|
|
||||||
statuses: notifications.map(item => item.status).filter(status => !!status),
|
|
||||||
next,
|
next,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
import { importFetchedStatuses } from './importer';
|
||||||
|
|
||||||
export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST';
|
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_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS';
|
||||||
|
@ -11,6 +12,7 @@ export function fetchPinnedStatuses() {
|
||||||
dispatch(fetchPinnedStatusesRequest());
|
dispatch(fetchPinnedStatusesRequest());
|
||||||
|
|
||||||
api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => {
|
api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => {
|
||||||
|
dispatch(importFetchedStatuses(response.data));
|
||||||
dispatch(fetchPinnedStatusesSuccess(response.data, null));
|
dispatch(fetchPinnedStatusesSuccess(response.data, null));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchPinnedStatusesFail(error));
|
dispatch(fetchPinnedStatusesFail(error));
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
import { fetchRelationships } from './accounts';
|
import { fetchRelationships } from './accounts';
|
||||||
|
import { importFetchedAccounts, importFetchedStatuses } from './importer';
|
||||||
|
|
||||||
export const SEARCH_CHANGE = 'SEARCH_CHANGE';
|
export const SEARCH_CHANGE = 'SEARCH_CHANGE';
|
||||||
export const SEARCH_CLEAR = 'SEARCH_CLEAR';
|
export const SEARCH_CLEAR = 'SEARCH_CLEAR';
|
||||||
|
@ -38,6 +39,14 @@ export function submitSearch() {
|
||||||
resolve: true,
|
resolve: true,
|
||||||
},
|
},
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
|
if (response.data.accounts) {
|
||||||
|
dispatch(importFetchedAccounts(response.data.accounts));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data.statuses) {
|
||||||
|
dispatch(importFetchedStatuses(response.data.statuses));
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(fetchSearchSuccess(response.data));
|
dispatch(fetchSearchSuccess(response.data));
|
||||||
dispatch(fetchRelationships(response.data.accounts.map(item => item.id)));
|
dispatch(fetchRelationships(response.data.accounts.map(item => item.id)));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
@ -56,8 +65,6 @@ export function fetchSearchSuccess(results) {
|
||||||
return {
|
return {
|
||||||
type: SEARCH_FETCH_SUCCESS,
|
type: SEARCH_FETCH_SUCCESS,
|
||||||
results,
|
results,
|
||||||
accounts: results.accounts,
|
|
||||||
statuses: results.statuses,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
import asyncDB from '../db/async';
|
||||||
|
import { evictStatus } from '../db/modifier';
|
||||||
|
|
||||||
import { deleteFromTimelines } from './timelines';
|
import { deleteFromTimelines } from './timelines';
|
||||||
import { fetchStatusCard } from './cards';
|
import { fetchStatusCard } from './cards';
|
||||||
|
import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus } from './importer';
|
||||||
|
|
||||||
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
|
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
|
||||||
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
|
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
|
||||||
|
@ -34,6 +37,48 @@ export function fetchStatusRequest(id, skipLoading) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getFromDB(dispatch, getState, accountIndex, index, id) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = index.get(id);
|
||||||
|
|
||||||
|
request.onerror = reject;
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
if (!request.result) {
|
||||||
|
reject();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(importStatus(request.result));
|
||||||
|
|
||||||
|
if (getState().getIn(['accounts', request.result.account], null) === null) {
|
||||||
|
promises.push(new Promise((accountResolve, accountReject) => {
|
||||||
|
const accountRequest = accountIndex.get(request.result.account);
|
||||||
|
|
||||||
|
accountRequest.onerror = accountReject;
|
||||||
|
accountRequest.onsuccess = () => {
|
||||||
|
if (!request.result) {
|
||||||
|
accountReject();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(importAccount(accountRequest.result));
|
||||||
|
accountResolve();
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.result.reblog && getState().getIn(['statuses', request.result.reblog], null) === null) {
|
||||||
|
promises.push(getFromDB(dispatch, getState, accountIndex, index, request.result.reblog));
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(Promise.all(promises));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchStatus(id) {
|
export function fetchStatus(id) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const skipLoading = getState().getIn(['statuses', id], null) !== null;
|
const skipLoading = getState().getIn(['statuses', id], null) !== null;
|
||||||
|
@ -47,18 +92,26 @@ export function fetchStatus(id) {
|
||||||
|
|
||||||
dispatch(fetchStatusRequest(id, skipLoading));
|
dispatch(fetchStatusRequest(id, skipLoading));
|
||||||
|
|
||||||
api(getState).get(`/api/v1/statuses/${id}`).then(response => {
|
asyncDB.then(db => {
|
||||||
dispatch(fetchStatusSuccess(response.data, skipLoading));
|
const transaction = db.transaction(['accounts', 'statuses'], 'read');
|
||||||
}).catch(error => {
|
const accountIndex = transaction.objectStore('accounts').index('id');
|
||||||
|
const index = transaction.objectStore('statuses').index('id');
|
||||||
|
|
||||||
|
return getFromDB(dispatch, getState, accountIndex, index, id);
|
||||||
|
}).then(() => {
|
||||||
|
dispatch(fetchStatusSuccess(skipLoading));
|
||||||
|
}, () => api(getState).get(`/api/v1/statuses/${id}`).then(response => {
|
||||||
|
dispatch(importFetchedStatus(response.data));
|
||||||
|
dispatch(fetchStatusSuccess(skipLoading));
|
||||||
|
})).catch(error => {
|
||||||
dispatch(fetchStatusFail(id, error, skipLoading));
|
dispatch(fetchStatusFail(id, error, skipLoading));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function fetchStatusSuccess(status, skipLoading) {
|
export function fetchStatusSuccess(skipLoading) {
|
||||||
return {
|
return {
|
||||||
type: STATUS_FETCH_SUCCESS,
|
type: STATUS_FETCH_SUCCESS,
|
||||||
status,
|
|
||||||
skipLoading,
|
skipLoading,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -78,6 +131,7 @@ export function deleteStatus(id) {
|
||||||
dispatch(deleteStatusRequest(id));
|
dispatch(deleteStatusRequest(id));
|
||||||
|
|
||||||
api(getState).delete(`/api/v1/statuses/${id}`).then(() => {
|
api(getState).delete(`/api/v1/statuses/${id}`).then(() => {
|
||||||
|
evictStatus(id);
|
||||||
dispatch(deleteStatusSuccess(id));
|
dispatch(deleteStatusSuccess(id));
|
||||||
dispatch(deleteFromTimelines(id));
|
dispatch(deleteFromTimelines(id));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
@ -113,6 +167,7 @@ export function fetchContext(id) {
|
||||||
dispatch(fetchContextRequest(id));
|
dispatch(fetchContextRequest(id));
|
||||||
|
|
||||||
api(getState).get(`/api/v1/statuses/${id}/context`).then(response => {
|
api(getState).get(`/api/v1/statuses/${id}/context`).then(response => {
|
||||||
|
dispatch(importFetchedStatuses(response.data.ancestors.concat(response.data.descendants)));
|
||||||
dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants));
|
dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants));
|
||||||
|
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Iterable, fromJS } from 'immutable';
|
import { Iterable, fromJS } from 'immutable';
|
||||||
import { hydrateCompose } from './compose';
|
import { hydrateCompose } from './compose';
|
||||||
|
import { importFetchedAccounts } from './importer';
|
||||||
|
|
||||||
export const STORE_HYDRATE = 'STORE_HYDRATE';
|
export const STORE_HYDRATE = 'STORE_HYDRATE';
|
||||||
export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
|
export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
|
||||||
|
@ -18,5 +19,6 @@ export function hydrateStore(rawState) {
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch(hydrateCompose());
|
dispatch(hydrateCompose());
|
||||||
|
dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,11 +2,10 @@ import { connectStream } from '../stream';
|
||||||
import {
|
import {
|
||||||
updateTimeline,
|
updateTimeline,
|
||||||
deleteFromTimelines,
|
deleteFromTimelines,
|
||||||
refreshHomeTimeline,
|
expandHomeTimeline,
|
||||||
connectTimeline,
|
|
||||||
disconnectTimeline,
|
disconnectTimeline,
|
||||||
} from './timelines';
|
} from './timelines';
|
||||||
import { updateNotifications, refreshNotifications } from './notifications';
|
import { updateNotifications, expandNotifications } from './notifications';
|
||||||
import { getLocale } from '../locales';
|
import { getLocale } from '../locales';
|
||||||
|
|
||||||
const { messages } = getLocale();
|
const { messages } = getLocale();
|
||||||
|
@ -16,10 +15,6 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null)
|
||||||
return connectStream (path, pollingRefresh, (dispatch, getState) => {
|
return connectStream (path, pollingRefresh, (dispatch, getState) => {
|
||||||
const locale = getState().getIn(['meta', 'locale']);
|
const locale = getState().getIn(['meta', 'locale']);
|
||||||
return {
|
return {
|
||||||
onConnect() {
|
|
||||||
dispatch(connectTimeline(timelineId));
|
|
||||||
},
|
|
||||||
|
|
||||||
onDisconnect() {
|
onDisconnect() {
|
||||||
dispatch(disconnectTimeline(timelineId));
|
dispatch(disconnectTimeline(timelineId));
|
||||||
},
|
},
|
||||||
|
@ -42,8 +37,8 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null)
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshHomeTimelineAndNotification (dispatch) {
|
function refreshHomeTimelineAndNotification (dispatch) {
|
||||||
dispatch(refreshHomeTimeline());
|
dispatch(expandHomeTimeline());
|
||||||
dispatch(refreshNotifications());
|
dispatch(expandNotifications());
|
||||||
}
|
}
|
||||||
|
|
||||||
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
|
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
|
||||||
|
|
|
@ -1,35 +1,20 @@
|
||||||
|
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
|
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
|
||||||
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
|
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_REQUEST = 'TIMELINE_EXPAND_REQUEST';
|
||||||
export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS';
|
export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS';
|
||||||
export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL';
|
export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL';
|
||||||
|
|
||||||
export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
|
export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
|
||||||
|
|
||||||
export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
|
|
||||||
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
|
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
|
||||||
|
|
||||||
export const TIMELINE_CONTEXT_UPDATE = 'CONTEXT_UPDATE';
|
export const TIMELINE_CONTEXT_UPDATE = 'CONTEXT_UPDATE';
|
||||||
|
|
||||||
export function refreshTimelineSuccess(timeline, statuses, skipLoading, next, partial) {
|
|
||||||
return {
|
|
||||||
type: TIMELINE_REFRESH_SUCCESS,
|
|
||||||
timeline,
|
|
||||||
statuses,
|
|
||||||
skipLoading,
|
|
||||||
next,
|
|
||||||
partial,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function updateTimeline(timeline, status) {
|
export function updateTimeline(timeline, status) {
|
||||||
return (dispatch, getState) => {
|
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 references = status.reblog ? getState().get('statuses').filter((item, itemId) => (itemId === status.reblog.id || item.get('reblog') === status.reblog.id)).map((_, itemId) => itemId) : [];
|
||||||
|
@ -44,6 +29,8 @@ export function updateTimeline(timeline, status) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispatch(importFetchedStatus(status));
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: TIMELINE_UPDATE,
|
type: TIMELINE_UPDATE,
|
||||||
timeline,
|
timeline,
|
||||||
|
@ -77,95 +64,34 @@ export function deleteFromTimelines(id) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
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') && !timeline.get('isPartial'))) {
|
|
||||||
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 => {
|
|
||||||
if (response.status === 206) {
|
|
||||||
dispatch(refreshTimelineSuccess(timelineId, [], skipLoading, null, true));
|
|
||||||
} else {
|
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
|
||||||
dispatch(refreshTimelineSuccess(timelineId, response.data, skipLoading, next ? next.uri : null, false));
|
|
||||||
}
|
|
||||||
}).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 refreshAccountTimeline = (accountId, withReplies) => refreshTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies });
|
|
||||||
export const refreshAccountFeaturedTimeline = accountId => refreshTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
|
|
||||||
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 const refreshListTimeline = id => refreshTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);
|
|
||||||
|
|
||||||
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 = {}) {
|
export function expandTimeline(timelineId, path, params = {}) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
|
const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
|
||||||
const ids = timeline.get('items', ImmutableList());
|
|
||||||
|
|
||||||
if (timeline.get('isLoading') || ids.size === 0) {
|
if (timeline.get('isLoading')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
params.max_id = ids.last();
|
|
||||||
params.limit = 10;
|
|
||||||
|
|
||||||
dispatch(expandTimelineRequest(timelineId));
|
dispatch(expandTimelineRequest(timelineId));
|
||||||
|
|
||||||
api(getState).get(path, { params }).then(response => {
|
api(getState).get(path, { params }).then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null));
|
dispatch(importFetchedStatuses(response.data));
|
||||||
|
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(expandTimelineFail(timelineId, error));
|
dispatch(expandTimelineFail(timelineId, error));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const expandHomeTimeline = () => expandTimeline('home', '/api/v1/timelines/home');
|
export const expandHomeTimeline = ({ maxId } = {}) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId });
|
||||||
export const expandPublicTimeline = () => expandTimeline('public', '/api/v1/timelines/public');
|
export const expandPublicTimeline = ({ maxId } = {}) => expandTimeline('public', '/api/v1/timelines/public', { max_id: maxId });
|
||||||
export const expandCommunityTimeline = () => expandTimeline('community', '/api/v1/timelines/public', { local: true });
|
export const expandCommunityTimeline = ({ maxId } = {}) => expandTimeline('community', '/api/v1/timelines/public', { local: true, max_id: maxId });
|
||||||
export const expandAccountTimeline = (accountId, withReplies) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies });
|
export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
|
||||||
export const expandAccountMediaTimeline = accountId => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
|
export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
|
||||||
export const expandHashtagTimeline = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
|
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true });
|
||||||
export const expandListTimeline = id => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);
|
export const expandHashtagTimeline = (hashtag, { maxId } = {}) => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId });
|
||||||
|
export const expandListTimeline = (id, { maxId } = {}) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId });
|
||||||
|
|
||||||
export function expandTimelineRequest(timeline) {
|
export function expandTimelineRequest(timeline) {
|
||||||
return {
|
return {
|
||||||
|
@ -174,12 +100,13 @@ export function expandTimelineRequest(timeline) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function expandTimelineSuccess(timeline, statuses, next) {
|
export function expandTimelineSuccess(timeline, statuses, next, partial) {
|
||||||
return {
|
return {
|
||||||
type: TIMELINE_EXPAND_SUCCESS,
|
type: TIMELINE_EXPAND_SUCCESS,
|
||||||
timeline,
|
timeline,
|
||||||
statuses,
|
statuses,
|
||||||
next,
|
next,
|
||||||
|
partial,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -199,13 +126,6 @@ export function scrollTopTimeline(timeline, top) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function connectTimeline(timeline) {
|
|
||||||
return {
|
|
||||||
type: TIMELINE_CONNECT,
|
|
||||||
timeline,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function disconnectTimeline(timeline) {
|
export function disconnectTimeline(timeline) {
|
||||||
return {
|
return {
|
||||||
type: TIMELINE_DISCONNECT,
|
type: TIMELINE_DISCONNECT,
|
||||||
|
|
|
@ -6,6 +6,7 @@ export default class LoadMore extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
visible: PropTypes.bool,
|
visible: PropTypes.bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,10 +15,10 @@ export default class LoadMore extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { visible } = this.props;
|
const { disabled, visible } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className='load-more' disabled={!visible} style={{ visibility: visible ? 'visible' : 'hidden' }} onClick={this.props.onClick}>
|
<button className='load-more' disabled={disabled || !visible} style={{ visibility: visible ? 'visible' : 'hidden' }} onClick={this.props.onClick}>
|
||||||
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
|
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,10 +14,6 @@ const messages = defineMessages({
|
||||||
|
|
||||||
class Item extends React.PureComponent {
|
class Item extends React.PureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
|
||||||
router: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
attachment: ImmutablePropTypes.map.isRequired,
|
attachment: ImmutablePropTypes.map.isRequired,
|
||||||
standalone: PropTypes.bool,
|
standalone: PropTypes.bool,
|
||||||
|
@ -53,7 +49,7 @@ class Item extends React.PureComponent {
|
||||||
handleClick = (e) => {
|
handleClick = (e) => {
|
||||||
const { index, onClick } = this.props;
|
const { index, onClick } = this.props;
|
||||||
|
|
||||||
if (this.context.router && e.button === 0) {
|
if (e.button === 0) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onClick(index);
|
onClick(index);
|
||||||
}
|
}
|
||||||
|
|
84
app/javascript/mastodon/components/modal_root.js
Normal file
84
app/javascript/mastodon/components/modal_root.js
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
export default class ModalRoot extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
revealed: !!this.props.children,
|
||||||
|
};
|
||||||
|
|
||||||
|
activeElement = this.state.revealed ? document.activeElement : null;
|
||||||
|
|
||||||
|
handleKeyUp = (e) => {
|
||||||
|
if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27)
|
||||||
|
&& !!this.props.children) {
|
||||||
|
this.props.onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
window.addEventListener('keyup', this.handleKeyUp, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
if (!!nextProps.children && !this.props.children) {
|
||||||
|
this.activeElement = document.activeElement;
|
||||||
|
|
||||||
|
this.getSiblings().forEach(sibling => sibling.setAttribute('inert', true));
|
||||||
|
} else if (!nextProps.children) {
|
||||||
|
this.setState({ revealed: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate (prevProps) {
|
||||||
|
if (!this.props.children && !!prevProps.children) {
|
||||||
|
this.getSiblings().forEach(sibling => sibling.removeAttribute('inert'));
|
||||||
|
this.activeElement.focus();
|
||||||
|
this.activeElement = null;
|
||||||
|
}
|
||||||
|
if (this.props.children) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { children, onClose } = this.props;
|
||||||
|
const { revealed } = this.state;
|
||||||
|
const visible = !!children;
|
||||||
|
|
||||||
|
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'>{children}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -17,7 +17,7 @@ export default class ScrollableList extends PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
scrollKey: PropTypes.string.isRequired,
|
scrollKey: PropTypes.string.isRequired,
|
||||||
onLoadMore: PropTypes.func.isRequired,
|
onLoadMore: PropTypes.func,
|
||||||
onScrollToTop: PropTypes.func,
|
onScrollToTop: PropTypes.func,
|
||||||
onScroll: PropTypes.func,
|
onScroll: PropTypes.func,
|
||||||
trackScroll: PropTypes.bool,
|
trackScroll: PropTypes.bool,
|
||||||
|
@ -148,11 +148,11 @@ export default class ScrollableList extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props;
|
const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage, onLoadMore } = this.props;
|
||||||
const { fullscreen } = this.state;
|
const { fullscreen } = this.state;
|
||||||
const childrenCount = React.Children.count(children);
|
const childrenCount = React.Children.count(children);
|
||||||
|
|
||||||
const loadMore = (hasMore && childrenCount > 0) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
|
const loadMore = (hasMore && childrenCount > 0 && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
|
||||||
let scrollableArea = null;
|
let scrollableArea = null;
|
||||||
|
|
||||||
if (isLoading || childrenCount > 0 || !emptyMessage) {
|
if (isLoading || childrenCount > 0 || !emptyMessage) {
|
||||||
|
|
|
@ -158,7 +158,7 @@ export default class StatusContent extends React.PureComponent {
|
||||||
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
|
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
|
||||||
<span dangerouslySetInnerHTML={spoilerContent} />
|
<span dangerouslySetInnerHTML={spoilerContent} />
|
||||||
{' '}
|
{' '}
|
||||||
<button tabIndex='0' className='status__content__spoiler-link' onClick={this.handleSpoilerClick}>{toggleText}</button>
|
<button tabIndex='0' className={`status__content__spoiler-link ${hidden ? 'status__content__spoiler-link--show-more' : 'status__content__spoiler-link--show-less'}`} onClick={this.handleSpoilerClick}>{toggleText}</button>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{mentionsPlaceholder}
|
{mentionsPlaceholder}
|
||||||
|
|
|
@ -1,11 +1,31 @@
|
||||||
|
import { debounce } from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import StatusContainer from '../containers/status_container';
|
import StatusContainer from '../containers/status_container';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import LoadMore from './load_more';
|
||||||
import ScrollableList from './scrollable_list';
|
import ScrollableList from './scrollable_list';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
class LoadGap extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
maxId: PropTypes.string,
|
||||||
|
onClick: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = () => {
|
||||||
|
this.props.onClick(this.props.maxId);
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return <LoadMore onClick={this.handleClick} disabled={this.props.disabled} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
export default class StatusList extends ImmutablePureComponent {
|
export default class StatusList extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -38,6 +58,10 @@ export default class StatusList extends ImmutablePureComponent {
|
||||||
this._selectChild(elementIndex);
|
this._selectChild(elementIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleLoadOlder = debounce(() => {
|
||||||
|
this.props.onLoadMore(this.props.statusIds.last());
|
||||||
|
}, 300, { leading: true })
|
||||||
|
|
||||||
_selectChild (index) {
|
_selectChild (index) {
|
||||||
const element = this.node.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
const element = this.node.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||||
|
|
||||||
|
@ -51,7 +75,7 @@ export default class StatusList extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { statusIds, featuredStatusIds, ...other } = this.props;
|
const { statusIds, featuredStatusIds, onLoadMore, ...other } = this.props;
|
||||||
const { isLoading, isPartial } = other;
|
const { isLoading, isPartial } = other;
|
||||||
|
|
||||||
if (isPartial) {
|
if (isPartial) {
|
||||||
|
@ -70,7 +94,14 @@ export default class StatusList extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
let scrollableContent = (isLoading || statusIds.size > 0) ? (
|
let scrollableContent = (isLoading || statusIds.size > 0) ? (
|
||||||
statusIds.map(statusId => (
|
statusIds.map((statusId, index) => statusId === null ? (
|
||||||
|
<LoadGap
|
||||||
|
key={'gap:' + statusIds.get(index + 1)}
|
||||||
|
disabled={isLoading}
|
||||||
|
maxId={index > 0 ? statusIds.get(index - 1) : null}
|
||||||
|
onClick={onLoadMore}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<StatusContainer
|
<StatusContainer
|
||||||
key={statusId}
|
key={statusId}
|
||||||
id={statusId}
|
id={statusId}
|
||||||
|
@ -93,7 +124,7 @@ export default class StatusList extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollableList {...other} ref={this.setRef}>
|
<ScrollableList {...other} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}>
|
||||||
{scrollableContent}
|
{scrollableContent}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||||
|
import { getLocale } from '../locales';
|
||||||
|
import MediaGallery from '../components/media_gallery';
|
||||||
|
import ModalRoot from '../components/modal_root';
|
||||||
|
import MediaModal from '../features/ui/components/media_modal';
|
||||||
|
import { fromJS } from 'immutable';
|
||||||
|
|
||||||
|
const { localeData, messages } = getLocale();
|
||||||
|
addLocaleData(localeData);
|
||||||
|
|
||||||
|
export default class MediaGalleriesContainer extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
locale: PropTypes.string.isRequired,
|
||||||
|
galleries: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
media: null,
|
||||||
|
index: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleOpenMedia = (media, index) => {
|
||||||
|
document.body.classList.add('media-gallery-standalone__body');
|
||||||
|
this.setState({ media, index });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCloseMedia = () => {
|
||||||
|
document.body.classList.remove('media-gallery-standalone__body');
|
||||||
|
this.setState({ media: null, index: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { locale, galleries } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IntlProvider locale={locale} messages={messages}>
|
||||||
|
<React.Fragment>
|
||||||
|
{[].map.call(galleries, gallery => {
|
||||||
|
const { media, ...props } = JSON.parse(gallery.getAttribute('data-props'));
|
||||||
|
|
||||||
|
return ReactDOM.createPortal(
|
||||||
|
<MediaGallery
|
||||||
|
{...props}
|
||||||
|
media={fromJS(media)}
|
||||||
|
onOpenMedia={this.handleOpenMedia}
|
||||||
|
/>,
|
||||||
|
gallery
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<ModalRoot onClose={this.handleCloseMedia}>
|
||||||
|
{this.state.media === null || this.state.index === null ? null : (
|
||||||
|
<MediaModal
|
||||||
|
media={this.state.media}
|
||||||
|
index={this.state.index}
|
||||||
|
onClose={this.handleCloseMedia}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ModalRoot>
|
||||||
|
</React.Fragment>
|
||||||
|
</IntlProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,34 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
|
||||||
import { getLocale } from '../locales';
|
|
||||||
import MediaGallery from '../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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
28
app/javascript/mastodon/db/async.js
Normal file
28
app/javascript/mastodon/db/async.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { me } from '../initial_state';
|
||||||
|
|
||||||
|
export default new Promise((resolve, reject) => {
|
||||||
|
// Microsoft Edge 17 does not support getAll according to:
|
||||||
|
// Catalog of standard and vendor APIs across browsers - Microsoft Edge Development
|
||||||
|
// https://developer.microsoft.com/en-us/microsoft-edge/platform/catalog/?q=specName%3Aindexeddb
|
||||||
|
if (!me || !('getAll' in IDBObjectStore.prototype)) {
|
||||||
|
reject();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = indexedDB.open('mastodon:' + me);
|
||||||
|
|
||||||
|
request.onerror = reject;
|
||||||
|
request.onsuccess = ({ target }) => resolve(target.result);
|
||||||
|
|
||||||
|
request.onupgradeneeded = ({ target }) => {
|
||||||
|
const accounts = target.result.createObjectStore('accounts', { autoIncrement: true });
|
||||||
|
const statuses = target.result.createObjectStore('statuses', { autoIncrement: true });
|
||||||
|
|
||||||
|
accounts.createIndex('id', 'id', { unique: true });
|
||||||
|
accounts.createIndex('moved', 'moved');
|
||||||
|
|
||||||
|
statuses.createIndex('id', 'id', { unique: true });
|
||||||
|
statuses.createIndex('account', 'account');
|
||||||
|
statuses.createIndex('reblog', 'reblog');
|
||||||
|
};
|
||||||
|
});
|
93
app/javascript/mastodon/db/modifier.js
Normal file
93
app/javascript/mastodon/db/modifier.js
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import asyncDB from './async';
|
||||||
|
|
||||||
|
const limit = 1024;
|
||||||
|
|
||||||
|
function put(name, objects, callback) {
|
||||||
|
asyncDB.then(db => {
|
||||||
|
const putTransaction = db.transaction(name, 'readwrite');
|
||||||
|
const putStore = putTransaction.objectStore(name);
|
||||||
|
const putIndex = putStore.index('id');
|
||||||
|
|
||||||
|
objects.forEach(object => {
|
||||||
|
function add() {
|
||||||
|
putStore.add(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
putIndex.getKey(object.id).onsuccess = retrieval => {
|
||||||
|
if (retrieval.target.result) {
|
||||||
|
putStore.delete(retrieval.target.result).onsuccess = add;
|
||||||
|
} else {
|
||||||
|
add();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
putTransaction.oncomplete = () => {
|
||||||
|
const readTransaction = db.transaction(name, 'readonly');
|
||||||
|
const readStore = readTransaction.objectStore(name);
|
||||||
|
|
||||||
|
readStore.count().onsuccess = count => {
|
||||||
|
const excess = count.target.result - limit;
|
||||||
|
|
||||||
|
if (excess > 0) {
|
||||||
|
readStore.getAll(null, excess).onsuccess =
|
||||||
|
retrieval => callback(retrieval.target.result.map(({ id }) => id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function evictAccounts(ids) {
|
||||||
|
asyncDB.then(db => {
|
||||||
|
const transaction = db.transaction(['accounts', 'statuses'], 'readwrite');
|
||||||
|
const accounts = transaction.objectStore('accounts');
|
||||||
|
const accountsIdIndex = accounts.index('id');
|
||||||
|
const accountsMovedIndex = accounts.index('moved');
|
||||||
|
const statuses = transaction.objectStore('statuses');
|
||||||
|
const statusesIndex = statuses.index('account');
|
||||||
|
|
||||||
|
function evict(toEvict) {
|
||||||
|
toEvict.forEach(id => {
|
||||||
|
accountsMovedIndex.getAllKeys(id).onsuccess =
|
||||||
|
({ target }) => evict(target.result);
|
||||||
|
|
||||||
|
statusesIndex.getAll(id).onsuccess =
|
||||||
|
({ target }) => evictStatuses(target.result.map(({ id }) => id));
|
||||||
|
|
||||||
|
accountsIdIndex.getKey(id).onsuccess =
|
||||||
|
({ target }) => target.result && accounts.delete(target.result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
evict(ids);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function evictStatus(id) {
|
||||||
|
return evictStatuses([id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function evictStatuses(ids) {
|
||||||
|
asyncDB.then(db => {
|
||||||
|
const store = db.transaction('statuses', 'readwrite').objectStore('statuses');
|
||||||
|
const idIndex = store.index('id');
|
||||||
|
const reblogIndex = store.index('reblog');
|
||||||
|
|
||||||
|
ids.forEach(id => {
|
||||||
|
reblogIndex.getAllKeys(id).onsuccess =
|
||||||
|
({ target }) => target.result.forEach(reblogKey => store.delete(reblogKey));
|
||||||
|
|
||||||
|
idIndex.getKey(id).onsuccess =
|
||||||
|
({ target }) => target.result && store.delete(target.result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function putAccounts(records) {
|
||||||
|
put('accounts', records, evictAccounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function putStatuses(records) {
|
||||||
|
put('statuses', records, evictStatuses);
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ import { connect } from 'react-redux';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { fetchAccount } from '../../actions/accounts';
|
import { fetchAccount } from '../../actions/accounts';
|
||||||
import { refreshAccountMediaTimeline, expandAccountMediaTimeline } from '../../actions/timelines';
|
import { expandAccountMediaTimeline } from '../../actions/timelines';
|
||||||
import LoadingIndicator from '../../components/loading_indicator';
|
import LoadingIndicator from '../../components/loading_indicator';
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
import ColumnBackButton from '../../components/column_back_button';
|
import ColumnBackButton from '../../components/column_back_button';
|
||||||
|
@ -17,9 +17,31 @@ import LoadMore from '../../components/load_more';
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
medias: getAccountGallery(state, props.params.accountId),
|
medias: getAccountGallery(state, props.params.accountId),
|
||||||
isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']),
|
isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']),
|
||||||
hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}:media`, 'next']),
|
hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
class LoadMoreMedia extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
maxId: PropTypes.string,
|
||||||
|
onLoadMore: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleLoadMore = () => {
|
||||||
|
this.props.onLoadMore(this.props.maxId);
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<LoadMore
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
onLoadMore={this.handleLoadMore}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@connect(mapStateToProps)
|
@connect(mapStateToProps)
|
||||||
export default class AccountGallery extends ImmutablePureComponent {
|
export default class AccountGallery extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
@ -33,19 +55,19 @@ export default class AccountGallery extends ImmutablePureComponent {
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.props.dispatch(fetchAccount(this.props.params.accountId));
|
this.props.dispatch(fetchAccount(this.props.params.accountId));
|
||||||
this.props.dispatch(refreshAccountMediaTimeline(this.props.params.accountId));
|
this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
|
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
|
||||||
this.props.dispatch(fetchAccount(nextProps.params.accountId));
|
this.props.dispatch(fetchAccount(nextProps.params.accountId));
|
||||||
this.props.dispatch(refreshAccountMediaTimeline(this.props.params.accountId));
|
this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleScrollToBottom = () => {
|
handleScrollToBottom = () => {
|
||||||
if (this.props.hasMore) {
|
if (this.props.hasMore) {
|
||||||
this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId));
|
this.handleLoadMore(this.props.medias.last().get('id'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +80,11 @@ export default class AccountGallery extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoadMore = (e) => {
|
handleLoadMore = maxId => {
|
||||||
|
this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId, { maxId }));
|
||||||
|
};
|
||||||
|
|
||||||
|
handleLoadOlder = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.handleScrollToBottom();
|
this.handleScrollToBottom();
|
||||||
}
|
}
|
||||||
|
@ -66,7 +92,7 @@ export default class AccountGallery extends ImmutablePureComponent {
|
||||||
render () {
|
render () {
|
||||||
const { medias, isLoading, hasMore } = this.props;
|
const { medias, isLoading, hasMore } = this.props;
|
||||||
|
|
||||||
let loadMore = null;
|
let loadOlder = null;
|
||||||
|
|
||||||
if (!medias && isLoading) {
|
if (!medias && isLoading) {
|
||||||
return (
|
return (
|
||||||
|
@ -77,7 +103,7 @@ export default class AccountGallery extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLoading && medias.size > 0 && hasMore) {
|
if (!isLoading && medias.size > 0 && hasMore) {
|
||||||
loadMore = <LoadMore onClick={this.handleLoadMore} />;
|
loadOlder = <LoadMore onClick={this.handleLoadOlder} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -89,13 +115,18 @@ export default class AccountGallery extends ImmutablePureComponent {
|
||||||
<HeaderContainer accountId={this.props.params.accountId} />
|
<HeaderContainer accountId={this.props.params.accountId} />
|
||||||
|
|
||||||
<div className='account-gallery__container'>
|
<div className='account-gallery__container'>
|
||||||
{medias.map(media => (
|
{medias.map((media, index) => media === null ? (
|
||||||
|
<LoadMoreMedia
|
||||||
|
key={'more:' + medias.getIn(index + 1, 'id')}
|
||||||
|
maxId={index > 0 ? medias.getIn(index - 1, 'id') : null}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<MediaItem
|
<MediaItem
|
||||||
key={media.get('id')}
|
key={media.get('id')}
|
||||||
media={media}
|
media={media}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{loadMore}
|
{loadOlder}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ScrollContainer>
|
</ScrollContainer>
|
||||||
|
|
|
@ -99,7 +99,7 @@ export default class Header extends ImmutablePureComponent {
|
||||||
{!hideTabs && (
|
{!hideTabs && (
|
||||||
<div className='account__section-headline'>
|
<div className='account__section-headline'>
|
||||||
<NavLink exact to={`/accounts/${account.get('id')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink>
|
<NavLink exact to={`/accounts/${account.get('id')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink>
|
||||||
<NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots with replies' /></NavLink>
|
<NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots and replies' /></NavLink>
|
||||||
<NavLink exact to={`/accounts/${account.get('id')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
|
<NavLink exact to={`/accounts/${account.get('id')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { connect } from 'react-redux';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { fetchAccount } from '../../actions/accounts';
|
import { fetchAccount } from '../../actions/accounts';
|
||||||
import { refreshAccountTimeline, refreshAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines';
|
import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines';
|
||||||
import StatusList from '../../components/status_list';
|
import StatusList from '../../components/status_list';
|
||||||
import LoadingIndicator from '../../components/loading_indicator';
|
import LoadingIndicator from '../../components/loading_indicator';
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
|
@ -19,7 +19,7 @@ const mapStateToProps = (state, { params: { accountId }, withReplies = false })
|
||||||
statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()),
|
statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()),
|
||||||
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()),
|
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()),
|
||||||
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
|
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
|
||||||
hasMore: !!state.getIn(['timelines', `account:${path}`, 'next']),
|
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -41,25 +41,23 @@ export default class AccountTimeline extends ImmutablePureComponent {
|
||||||
|
|
||||||
this.props.dispatch(fetchAccount(accountId));
|
this.props.dispatch(fetchAccount(accountId));
|
||||||
if (!withReplies) {
|
if (!withReplies) {
|
||||||
this.props.dispatch(refreshAccountFeaturedTimeline(accountId));
|
this.props.dispatch(expandAccountFeaturedTimeline(accountId));
|
||||||
}
|
}
|
||||||
this.props.dispatch(refreshAccountTimeline(accountId, withReplies));
|
this.props.dispatch(expandAccountTimeline(accountId, { withReplies }));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) {
|
if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) {
|
||||||
this.props.dispatch(fetchAccount(nextProps.params.accountId));
|
this.props.dispatch(fetchAccount(nextProps.params.accountId));
|
||||||
if (!nextProps.withReplies) {
|
if (!nextProps.withReplies) {
|
||||||
this.props.dispatch(refreshAccountFeaturedTimeline(nextProps.params.accountId));
|
this.props.dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId));
|
||||||
}
|
}
|
||||||
this.props.dispatch(refreshAccountTimeline(nextProps.params.accountId, nextProps.params.withReplies));
|
this.props.dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoadMore = () => {
|
handleLoadMore = maxId => {
|
||||||
if (!this.props.isLoading && this.props.hasMore) {
|
this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, withReplies: this.props.withReplies }));
|
||||||
this.props.dispatch(expandAccountTimeline(this.props.params.accountId, this.props.withReplies));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
|
|
@ -4,10 +4,7 @@ import PropTypes from 'prop-types';
|
||||||
import StatusListContainer from '../ui/containers/status_list_container';
|
import StatusListContainer from '../ui/containers/status_list_container';
|
||||||
import Column from '../../components/column';
|
import Column from '../../components/column';
|
||||||
import ColumnHeader from '../../components/column_header';
|
import ColumnHeader from '../../components/column_header';
|
||||||
import {
|
import { expandCommunityTimeline } from '../../actions/timelines';
|
||||||
refreshCommunityTimeline,
|
|
||||||
expandCommunityTimeline,
|
|
||||||
} from '../../actions/timelines';
|
|
||||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||||
|
@ -55,7 +52,7 @@ export default class CommunityTimeline extends React.PureComponent {
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
dispatch(refreshCommunityTimeline());
|
dispatch(expandCommunityTimeline());
|
||||||
this.disconnect = dispatch(connectCommunityStream());
|
this.disconnect = dispatch(connectCommunityStream());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,8 +67,8 @@ export default class CommunityTimeline extends React.PureComponent {
|
||||||
this.column = c;
|
this.column = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoadMore = () => {
|
handleLoadMore = maxId => {
|
||||||
this.props.dispatch(expandCommunityTimeline());
|
this.props.dispatch(expandCommunityTimeline({ maxId }));
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
@ -97,7 +94,7 @@ export default class CommunityTimeline extends React.PureComponent {
|
||||||
trackScroll={!pinned}
|
trackScroll={!pinned}
|
||||||
scrollKey={`community_timeline-${columnId}`}
|
scrollKey={`community_timeline-${columnId}`}
|
||||||
timelineId='community'
|
timelineId='community'
|
||||||
loadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
|
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
|
||||||
/>
|
/>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
@ -4,10 +4,7 @@ import PropTypes from 'prop-types';
|
||||||
import StatusListContainer from '../ui/containers/status_list_container';
|
import StatusListContainer from '../ui/containers/status_list_container';
|
||||||
import Column from '../../components/column';
|
import Column from '../../components/column';
|
||||||
import ColumnHeader from '../../components/column_header';
|
import ColumnHeader from '../../components/column_header';
|
||||||
import {
|
import { expandHashtagTimeline } from '../../actions/timelines';
|
||||||
refreshHashtagTimeline,
|
|
||||||
expandHashtagTimeline,
|
|
||||||
} from '../../actions/timelines';
|
|
||||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { connectHashtagStream } from '../../actions/streaming';
|
import { connectHashtagStream } from '../../actions/streaming';
|
||||||
|
@ -61,13 +58,13 @@ export default class HashtagTimeline extends React.PureComponent {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
const { id } = this.props.params;
|
const { id } = this.props.params;
|
||||||
|
|
||||||
dispatch(refreshHashtagTimeline(id));
|
dispatch(expandHashtagTimeline(id));
|
||||||
this._subscribe(dispatch, id);
|
this._subscribe(dispatch, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
if (nextProps.params.id !== this.props.params.id) {
|
if (nextProps.params.id !== this.props.params.id) {
|
||||||
this.props.dispatch(refreshHashtagTimeline(nextProps.params.id));
|
this.props.dispatch(expandHashtagTimeline(nextProps.params.id));
|
||||||
this._unsubscribe();
|
this._unsubscribe();
|
||||||
this._subscribe(this.props.dispatch, nextProps.params.id);
|
this._subscribe(this.props.dispatch, nextProps.params.id);
|
||||||
}
|
}
|
||||||
|
@ -81,8 +78,8 @@ export default class HashtagTimeline extends React.PureComponent {
|
||||||
this.column = c;
|
this.column = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoadMore = () => {
|
handleLoadMore = maxId => {
|
||||||
this.props.dispatch(expandHashtagTimeline(this.props.params.id));
|
this.props.dispatch(expandHashtagTimeline(this.props.params.id, { maxId }));
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
@ -108,7 +105,7 @@ export default class HashtagTimeline extends React.PureComponent {
|
||||||
trackScroll={!pinned}
|
trackScroll={!pinned}
|
||||||
scrollKey={`hashtag_timeline-${columnId}`}
|
scrollKey={`hashtag_timeline-${columnId}`}
|
||||||
timelineId={`hashtag:${id}`}
|
timelineId={`hashtag:${id}`}
|
||||||
loadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
|
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
|
||||||
/>
|
/>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { expandHomeTimeline, refreshHomeTimeline } from '../../actions/timelines';
|
import { expandHomeTimeline } from '../../actions/timelines';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import StatusListContainer from '../ui/containers/status_list_container';
|
import StatusListContainer from '../ui/containers/status_list_container';
|
||||||
import Column from '../../components/column';
|
import Column from '../../components/column';
|
||||||
|
@ -16,7 +16,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
|
hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
|
||||||
isPartial: state.getIn(['timelines', 'home', 'isPartial'], false),
|
isPartial: state.getIn(['timelines', 'home', 'items', 0], null) === null,
|
||||||
});
|
});
|
||||||
|
|
||||||
@connect(mapStateToProps)
|
@connect(mapStateToProps)
|
||||||
|
@ -55,8 +55,8 @@ export default class HomeTimeline extends React.PureComponent {
|
||||||
this.column = c;
|
this.column = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoadMore = () => {
|
handleLoadMore = maxId => {
|
||||||
this.props.dispatch(expandHomeTimeline());
|
this.props.dispatch(expandHomeTimeline({ maxId }));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
|
@ -78,7 +78,7 @@ export default class HomeTimeline extends React.PureComponent {
|
||||||
return;
|
return;
|
||||||
} else if (!wasPartial && isPartial) {
|
} else if (!wasPartial && isPartial) {
|
||||||
this.polling = setInterval(() => {
|
this.polling = setInterval(() => {
|
||||||
dispatch(refreshHomeTimeline());
|
dispatch(expandHomeTimeline());
|
||||||
}, 3000);
|
}, 3000);
|
||||||
} else if (wasPartial && !isPartial) {
|
} else if (wasPartial && !isPartial) {
|
||||||
this._stopPolling();
|
this._stopPolling();
|
||||||
|
@ -114,7 +114,7 @@ export default class HomeTimeline extends React.PureComponent {
|
||||||
<StatusListContainer
|
<StatusListContainer
|
||||||
trackScroll={!pinned}
|
trackScroll={!pinned}
|
||||||
scrollKey={`home_timeline-${columnId}`}
|
scrollKey={`home_timeline-${columnId}`}
|
||||||
loadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
timelineId='home'
|
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> }} />}
|
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> }} />}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -8,7 +8,7 @@ import ColumnHeader from '../../components/column_header';
|
||||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||||
import { connectListStream } from '../../actions/streaming';
|
import { connectListStream } from '../../actions/streaming';
|
||||||
import { refreshListTimeline, expandListTimeline } from '../../actions/timelines';
|
import { expandListTimeline } from '../../actions/timelines';
|
||||||
import { fetchList, deleteList } from '../../actions/lists';
|
import { fetchList, deleteList } from '../../actions/lists';
|
||||||
import { openModal } from '../../actions/modal';
|
import { openModal } from '../../actions/modal';
|
||||||
import MissingIndicator from '../../components/missing_indicator';
|
import MissingIndicator from '../../components/missing_indicator';
|
||||||
|
@ -67,7 +67,7 @@ export default class ListTimeline extends React.PureComponent {
|
||||||
const { id } = this.props.params;
|
const { id } = this.props.params;
|
||||||
|
|
||||||
dispatch(fetchList(id));
|
dispatch(fetchList(id));
|
||||||
dispatch(refreshListTimeline(id));
|
dispatch(expandListTimeline(id));
|
||||||
|
|
||||||
this.disconnect = dispatch(connectListStream(id));
|
this.disconnect = dispatch(connectListStream(id));
|
||||||
}
|
}
|
||||||
|
@ -83,9 +83,9 @@ export default class ListTimeline extends React.PureComponent {
|
||||||
this.column = c;
|
this.column = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoadMore = () => {
|
handleLoadMore = maxId => {
|
||||||
const { id } = this.props.params;
|
const { id } = this.props.params;
|
||||||
this.props.dispatch(expandListTimeline(id));
|
this.props.dispatch(expandListTimeline(id, { maxId }));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEditClick = () => {
|
handleEditClick = () => {
|
||||||
|
@ -164,7 +164,7 @@ export default class ListTimeline extends React.PureComponent {
|
||||||
trackScroll={!pinned}
|
trackScroll={!pinned}
|
||||||
scrollKey={`list_timeline-${columnId}`}
|
scrollKey={`list_timeline-${columnId}`}
|
||||||
timelineId={`list:${id}`}
|
timelineId={`list:${id}`}
|
||||||
loadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet. When members of this list post new statuses, they will appear here.' />}
|
emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet. When members of this list post new statuses, they will appear here.' />}
|
||||||
/>
|
/>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { createSelector } from 'reselect';
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import ScrollableList from '../../components/scrollable_list';
|
import ScrollableList from '../../components/scrollable_list';
|
||||||
|
import LoadMore from '../../components/load_more';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
|
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
|
||||||
|
@ -21,13 +22,31 @@ const messages = defineMessages({
|
||||||
const getNotifications = createSelector([
|
const getNotifications = createSelector([
|
||||||
state => ImmutableList(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()),
|
state => ImmutableList(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()),
|
||||||
state => state.getIn(['notifications', 'items']),
|
state => state.getIn(['notifications', 'items']),
|
||||||
], (excludedTypes, notifications) => notifications.filterNot(item => excludedTypes.includes(item.get('type'))));
|
], (excludedTypes, notifications) => notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type'))));
|
||||||
|
|
||||||
|
class LoadGap extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
maxId: PropTypes.string,
|
||||||
|
onClick: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = () => {
|
||||||
|
this.props.onClick(this.props.maxId);
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return <LoadMore onClick={this.handleClick} disabled={this.props.disabled} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
notifications: getNotifications(state),
|
notifications: getNotifications(state),
|
||||||
isLoading: state.getIn(['notifications', 'isLoading'], true),
|
isLoading: state.getIn(['notifications', 'isLoading'], true),
|
||||||
isUnread: state.getIn(['notifications', 'unread']) > 0,
|
isUnread: state.getIn(['notifications', 'unread']) > 0,
|
||||||
hasMore: !!state.getIn(['notifications', 'next']),
|
hasMore: state.getIn(['notifications', 'hasMore']),
|
||||||
});
|
});
|
||||||
|
|
||||||
@connect(mapStateToProps)
|
@connect(mapStateToProps)
|
||||||
|
@ -51,14 +70,19 @@ export default class Notifications extends React.PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
this.handleLoadMore.cancel();
|
this.handleLoadOlder.cancel();
|
||||||
this.handleScrollToTop.cancel();
|
this.handleScrollToTop.cancel();
|
||||||
this.handleScroll.cancel();
|
this.handleScroll.cancel();
|
||||||
this.props.dispatch(scrollTopNotifications(false));
|
this.props.dispatch(scrollTopNotifications(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoadMore = debounce(() => {
|
handleLoadGap = (maxId) => {
|
||||||
this.props.dispatch(expandNotifications());
|
this.props.dispatch(expandNotifications({ maxId }));
|
||||||
|
};
|
||||||
|
|
||||||
|
handleLoadOlder = debounce(() => {
|
||||||
|
const last = this.props.notifications.last();
|
||||||
|
this.props.dispatch(expandNotifications({ maxId: last && last.get('id') }));
|
||||||
}, 300, { leading: true });
|
}, 300, { leading: true });
|
||||||
|
|
||||||
handleScrollToTop = debounce(() => {
|
handleScrollToTop = debounce(() => {
|
||||||
|
@ -93,12 +117,12 @@ export default class Notifications extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMoveUp = id => {
|
handleMoveUp = id => {
|
||||||
const elementIndex = this.props.notifications.findIndex(item => item.get('id') === id) - 1;
|
const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1;
|
||||||
this._selectChild(elementIndex);
|
this._selectChild(elementIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMoveDown = id => {
|
handleMoveDown = id => {
|
||||||
const elementIndex = this.props.notifications.findIndex(item => item.get('id') === id) + 1;
|
const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1;
|
||||||
this._selectChild(elementIndex);
|
this._selectChild(elementIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,7 +144,14 @@ export default class Notifications extends React.PureComponent {
|
||||||
if (isLoading && this.scrollableContent) {
|
if (isLoading && this.scrollableContent) {
|
||||||
scrollableContent = this.scrollableContent;
|
scrollableContent = this.scrollableContent;
|
||||||
} else if (notifications.size > 0 || hasMore) {
|
} else if (notifications.size > 0 || hasMore) {
|
||||||
scrollableContent = notifications.map((item) => (
|
scrollableContent = notifications.map((item, index) => item === null ? (
|
||||||
|
<LoadGap
|
||||||
|
key={'gap:' + notifications.getIn([index + 1, 'id'])}
|
||||||
|
disabled={isLoading}
|
||||||
|
maxId={index > 0 ? notifications.getIn([index - 1, 'id']) : null}
|
||||||
|
onClick={this.handleLoadGap}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<NotificationContainer
|
<NotificationContainer
|
||||||
key={item.get('id')}
|
key={item.get('id')}
|
||||||
notification={item}
|
notification={item}
|
||||||
|
@ -142,7 +173,7 @@ export default class Notifications extends React.PureComponent {
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadOlder}
|
||||||
onScrollToTop={this.handleScrollToTop}
|
onScrollToTop={this.handleScrollToTop}
|
||||||
onScroll={this.handleScroll}
|
onScroll={this.handleScroll}
|
||||||
shouldUpdateScroll={shouldUpdateScroll}
|
shouldUpdateScroll={shouldUpdateScroll}
|
||||||
|
|
|
@ -4,10 +4,7 @@ import PropTypes from 'prop-types';
|
||||||
import StatusListContainer from '../ui/containers/status_list_container';
|
import StatusListContainer from '../ui/containers/status_list_container';
|
||||||
import Column from '../../components/column';
|
import Column from '../../components/column';
|
||||||
import ColumnHeader from '../../components/column_header';
|
import ColumnHeader from '../../components/column_header';
|
||||||
import {
|
import { expandPublicTimeline } from '../../actions/timelines';
|
||||||
refreshPublicTimeline,
|
|
||||||
expandPublicTimeline,
|
|
||||||
} from '../../actions/timelines';
|
|
||||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||||
|
@ -55,7 +52,7 @@ export default class PublicTimeline extends React.PureComponent {
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
dispatch(refreshPublicTimeline());
|
dispatch(expandPublicTimeline());
|
||||||
this.disconnect = dispatch(connectPublicStream());
|
this.disconnect = dispatch(connectPublicStream());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,8 +67,8 @@ export default class PublicTimeline extends React.PureComponent {
|
||||||
this.column = c;
|
this.column = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoadMore = () => {
|
handleLoadMore = maxId => {
|
||||||
this.props.dispatch(expandPublicTimeline());
|
this.props.dispatch(expandPublicTimeline({ maxId }));
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
@ -95,7 +92,7 @@ export default class PublicTimeline extends React.PureComponent {
|
||||||
|
|
||||||
<StatusListContainer
|
<StatusListContainer
|
||||||
timelineId='public'
|
timelineId='public'
|
||||||
loadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
trackScroll={!pinned}
|
trackScroll={!pinned}
|
||||||
scrollKey={`public_timeline-${columnId}`}
|
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' />}
|
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' />}
|
||||||
|
|
|
@ -2,10 +2,7 @@ import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import StatusListContainer from '../../ui/containers/status_list_container';
|
import StatusListContainer from '../../ui/containers/status_list_container';
|
||||||
import {
|
import { expandCommunityTimeline } from '../../../actions/timelines';
|
||||||
refreshCommunityTimeline,
|
|
||||||
expandCommunityTimeline,
|
|
||||||
} from '../../../actions/timelines';
|
|
||||||
import Column from '../../../components/column';
|
import Column from '../../../components/column';
|
||||||
import ColumnHeader from '../../../components/column_header';
|
import ColumnHeader from '../../../components/column_header';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
@ -35,7 +32,7 @@ export default class CommunityTimeline extends React.PureComponent {
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
dispatch(refreshCommunityTimeline());
|
dispatch(expandCommunityTimeline());
|
||||||
this.disconnect = dispatch(connectCommunityStream());
|
this.disconnect = dispatch(connectCommunityStream());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,8 +43,8 @@ export default class CommunityTimeline extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoadMore = () => {
|
handleLoadMore = maxId => {
|
||||||
this.props.dispatch(expandCommunityTimeline());
|
this.props.dispatch(expandCommunityTimeline({ maxId }));
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
@ -63,7 +60,7 @@ export default class CommunityTimeline extends React.PureComponent {
|
||||||
|
|
||||||
<StatusListContainer
|
<StatusListContainer
|
||||||
timelineId='community'
|
timelineId='community'
|
||||||
loadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
scrollKey='standalone_public_timeline'
|
scrollKey='standalone_public_timeline'
|
||||||
trackScroll={false}
|
trackScroll={false}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -2,10 +2,7 @@ import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import StatusListContainer from '../../ui/containers/status_list_container';
|
import StatusListContainer from '../../ui/containers/status_list_container';
|
||||||
import {
|
import { expandHashtagTimeline } from '../../../actions/timelines';
|
||||||
refreshHashtagTimeline,
|
|
||||||
expandHashtagTimeline,
|
|
||||||
} from '../../../actions/timelines';
|
|
||||||
import Column from '../../../components/column';
|
import Column from '../../../components/column';
|
||||||
import ColumnHeader from '../../../components/column_header';
|
import ColumnHeader from '../../../components/column_header';
|
||||||
import { connectHashtagStream } from '../../../actions/streaming';
|
import { connectHashtagStream } from '../../../actions/streaming';
|
||||||
|
@ -29,7 +26,7 @@ export default class HashtagTimeline extends React.PureComponent {
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { dispatch, hashtag } = this.props;
|
const { dispatch, hashtag } = this.props;
|
||||||
|
|
||||||
dispatch(refreshHashtagTimeline(hashtag));
|
dispatch(expandHashtagTimeline(hashtag));
|
||||||
this.disconnect = dispatch(connectHashtagStream(hashtag));
|
this.disconnect = dispatch(connectHashtagStream(hashtag));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,8 +37,8 @@ export default class HashtagTimeline extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoadMore = () => {
|
handleLoadMore = maxId => {
|
||||||
this.props.dispatch(expandHashtagTimeline(this.props.hashtag));
|
this.props.dispatch(expandHashtagTimeline(this.props.hashtag, { maxId }));
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
@ -59,7 +56,7 @@ export default class HashtagTimeline extends React.PureComponent {
|
||||||
trackScroll={false}
|
trackScroll={false}
|
||||||
scrollKey='standalone_hashtag_timeline'
|
scrollKey='standalone_hashtag_timeline'
|
||||||
timelineId={`hashtag:${hashtag}`}
|
timelineId={`hashtag:${hashtag}`}
|
||||||
loadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
/>
|
/>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,10 +2,7 @@ import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import StatusListContainer from '../../ui/containers/status_list_container';
|
import StatusListContainer from '../../ui/containers/status_list_container';
|
||||||
import {
|
import { expandPublicTimeline } from '../../../actions/timelines';
|
||||||
refreshPublicTimeline,
|
|
||||||
expandPublicTimeline,
|
|
||||||
} from '../../../actions/timelines';
|
|
||||||
import Column from '../../../components/column';
|
import Column from '../../../components/column';
|
||||||
import ColumnHeader from '../../../components/column_header';
|
import ColumnHeader from '../../../components/column_header';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
@ -35,7 +32,7 @@ export default class PublicTimeline extends React.PureComponent {
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
dispatch(refreshPublicTimeline());
|
dispatch(expandPublicTimeline());
|
||||||
this.disconnect = dispatch(connectPublicStream());
|
this.disconnect = dispatch(connectPublicStream());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,8 +43,8 @@ export default class PublicTimeline extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoadMore = () => {
|
handleLoadMore = maxId => {
|
||||||
this.props.dispatch(expandPublicTimeline());
|
this.props.dispatch(expandPublicTimeline({ maxId }));
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
@ -63,7 +60,7 @@ export default class PublicTimeline extends React.PureComponent {
|
||||||
|
|
||||||
<StatusListContainer
|
<StatusListContainer
|
||||||
timelineId='public'
|
timelineId='public'
|
||||||
loadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
scrollKey='standalone_public_timeline'
|
scrollKey='standalone_public_timeline'
|
||||||
trackScroll={false}
|
trackScroll={false}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import Base from '../../../components/modal_root';
|
||||||
import BundleContainer from '../containers/bundle_container';
|
import BundleContainer from '../containers/bundle_container';
|
||||||
import BundleModalError from './bundle_modal_error';
|
import BundleModalError from './bundle_modal_error';
|
||||||
import ModalLoading from './modal_loading';
|
import ModalLoading from './modal_loading';
|
||||||
|
@ -39,56 +40,6 @@ export default class ModalRoot extends React.PureComponent {
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
|
||||||
revealed: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleKeyUp = (e) => {
|
|
||||||
if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27)
|
|
||||||
&& !!this.props.type) {
|
|
||||||
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 => () => {
|
renderLoading = modalId => () => {
|
||||||
return ['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null;
|
return ['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null;
|
||||||
}
|
}
|
||||||
|
@ -101,28 +52,16 @@ export default class ModalRoot extends React.PureComponent {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { type, props, onClose } = this.props;
|
const { type, props, onClose } = this.props;
|
||||||
const { revealed } = this.state;
|
|
||||||
const visible = !!type;
|
const visible = !!type;
|
||||||
|
|
||||||
if (!visible) {
|
|
||||||
return (
|
return (
|
||||||
<div className='modal-root' ref={this.setRef} style={{ opacity: 0 }} />
|
<Base onClose={onClose}>
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 && (
|
{visible && (
|
||||||
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>
|
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>
|
||||||
{(SpecificComponent) => <SpecificComponent {...props} onClose={onClose} />}
|
{(SpecificComponent) => <SpecificComponent {...props} onClose={onClose} />}
|
||||||
</BundleContainer>
|
</BundleContainer>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Base>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { changeReportComment, changeReportForward, submitReport } from '../../../actions/reports';
|
import { changeReportComment, changeReportForward, submitReport } from '../../../actions/reports';
|
||||||
import { refreshAccountTimeline } from '../../../actions/timelines';
|
import { expandAccountTimeline } from '../../../actions/timelines';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { makeGetAccount } from '../../../selectors';
|
import { makeGetAccount } from '../../../selectors';
|
||||||
|
@ -64,12 +64,12 @@ export default class ReportModal extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.props.dispatch(refreshAccountTimeline(this.props.account.get('id')));
|
this.props.dispatch(expandAccountTimeline(this.props.account.get('id')));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
if (this.props.account !== nextProps.account && nextProps.account) {
|
if (this.props.account !== nextProps.account && nextProps.account) {
|
||||||
this.props.dispatch(refreshAccountTimeline(nextProps.account.get('id')));
|
this.props.dispatch(expandAccountTimeline(nextProps.account.get('id')));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,22 @@
|
||||||
|
import { injectIntl } from 'react-intl';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { NotificationStack } from 'react-notification';
|
import { NotificationStack } from 'react-notification';
|
||||||
import { dismissAlert } from '../../../actions/alerts';
|
import { dismissAlert } from '../../../actions/alerts';
|
||||||
import { getAlerts } from '../../../selectors';
|
import { getAlerts } from '../../../selectors';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = (state, { intl }) => {
|
||||||
notifications: getAlerts(state),
|
const notifications = getAlerts(state);
|
||||||
});
|
|
||||||
|
notifications.forEach(notification => ['title', 'message'].forEach(key => {
|
||||||
|
const value = notification[key];
|
||||||
|
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
notification[key] = intl.formatMessage(value);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { notifications };
|
||||||
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => {
|
const mapDispatchToProps = (dispatch) => {
|
||||||
return {
|
return {
|
||||||
|
@ -15,4 +26,4 @@ const mapDispatchToProps = (dispatch) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(NotificationStack);
|
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationStack));
|
||||||
|
|
|
@ -48,15 +48,13 @@ const makeMapStateToProps = () => {
|
||||||
statusIds: getStatusIds(state, { type: timelineId }),
|
statusIds: getStatusIds(state, { type: timelineId }),
|
||||||
isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true),
|
isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true),
|
||||||
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
|
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
|
||||||
hasMore: !!state.getIn(['timelines', timelineId, 'next']),
|
hasMore: state.getIn(['timelines', timelineId, 'hasMore']),
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapStateToProps;
|
return mapStateToProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { timelineId, loadMore }) => ({
|
const mapDispatchToProps = (dispatch, { timelineId }) => ({
|
||||||
|
|
||||||
onLoadMore: debounce(loadMore, 300, { leading: true }),
|
|
||||||
|
|
||||||
onScrollToTop: debounce(() => {
|
onScrollToTop: debounce(() => {
|
||||||
dispatch(scrollTopTimeline(timelineId, true));
|
dispatch(scrollTopTimeline(timelineId, true));
|
||||||
|
|
|
@ -10,8 +10,8 @@ import { Redirect, withRouter } from 'react-router-dom';
|
||||||
import { isMobile } from '../../is_mobile';
|
import { isMobile } from '../../is_mobile';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { uploadCompose, resetCompose } from '../../actions/compose';
|
import { uploadCompose, resetCompose } from '../../actions/compose';
|
||||||
import { refreshHomeTimeline } from '../../actions/timelines';
|
import { expandHomeTimeline } from '../../actions/timelines';
|
||||||
import { refreshNotifications } from '../../actions/notifications';
|
import { expandNotifications } from '../../actions/notifications';
|
||||||
import { clearHeight } from '../../actions/height_cache';
|
import { clearHeight } from '../../actions/height_cache';
|
||||||
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
|
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
|
||||||
import UploadArea from './components/upload_area';
|
import UploadArea from './components/upload_area';
|
||||||
|
@ -284,8 +284,8 @@ export default class UI extends React.PureComponent {
|
||||||
navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage);
|
navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.dispatch(refreshHomeTimeline());
|
this.props.dispatch(expandHomeTimeline());
|
||||||
this.props.dispatch(refreshNotifications());
|
this.props.dispatch(expandNotifications());
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "إلغاء الكتم عن @{name}",
|
"account.unmute": "إلغاء الكتم عن @{name}",
|
||||||
"account.unmute_notifications": "إلغاء كتم إخطارات @{name}",
|
"account.unmute_notifications": "إلغاء كتم إخطارات @{name}",
|
||||||
"account.view_full_profile": "عرض الملف الشخصي كاملا",
|
"account.view_full_profile": "عرض الملف الشخصي كاملا",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "يمكنك ضغط {combo} لتخطّي هذه في المرّة القادمة",
|
"boost_modal.combo": "يمكنك ضغط {combo} لتخطّي هذه في المرّة القادمة",
|
||||||
"bundle_column_error.body": "لقد وقع هناك خطأ أثناء عملية تحميل هذا العنصر.",
|
"bundle_column_error.body": "لقد وقع هناك خطأ أثناء عملية تحميل هذا العنصر.",
|
||||||
"bundle_column_error.retry": "إعادة المحاولة",
|
"bundle_column_error.retry": "إعادة المحاولة",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Unmute @{name}",
|
"account.unmute": "Unmute @{name}",
|
||||||
"account.unmute_notifications": "Unmute notifications from @{name}",
|
"account.unmute_notifications": "Unmute notifications from @{name}",
|
||||||
"account.view_full_profile": "View full profile",
|
"account.view_full_profile": "View full profile",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "You can press {combo} to skip this next time",
|
"boost_modal.combo": "You can press {combo} to skip this next time",
|
||||||
"bundle_column_error.body": "Something went wrong while loading this component.",
|
"bundle_column_error.body": "Something went wrong while loading this component.",
|
||||||
"bundle_column_error.retry": "Try again",
|
"bundle_column_error.retry": "Try again",
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"account.block": "Bloca @{name}",
|
"account.block": "Bloca @{name}",
|
||||||
"account.block_domain": "Amaga-ho tot de {domain}",
|
"account.block_domain": "Amaga-ho tot de {domain}",
|
||||||
"account.blocked": "Blocked",
|
"account.blocked": "Bloquejat",
|
||||||
"account.disclaimer_full": "La informació següent pot reflectir incompleta el perfil de l'usuari.",
|
"account.disclaimer_full": "La informació següent pot reflectir incompleta el perfil de l'usuari.",
|
||||||
"account.domain_blocked": "Domain hidden",
|
"account.domain_blocked": "Domini ocult",
|
||||||
"account.edit_profile": "Edita el perfil",
|
"account.edit_profile": "Edita el perfil",
|
||||||
"account.follow": "Segueix",
|
"account.follow": "Segueix",
|
||||||
"account.followers": "Seguidors",
|
"account.followers": "Seguidors",
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
"account.moved_to": "{name} s'ha mogut a:",
|
"account.moved_to": "{name} s'ha mogut a:",
|
||||||
"account.mute": "Silencia @{name}",
|
"account.mute": "Silencia @{name}",
|
||||||
"account.mute_notifications": "Notificacions desactivades de @{name}",
|
"account.mute_notifications": "Notificacions desactivades de @{name}",
|
||||||
"account.muted": "Muted",
|
"account.muted": "Silenciat",
|
||||||
"account.posts": "Toots",
|
"account.posts": "Toots",
|
||||||
"account.posts_with_replies": "Toots amb respostes",
|
"account.posts_with_replies": "Toots amb respostes",
|
||||||
"account.report": "Informe @{name}",
|
"account.report": "Informe @{name}",
|
||||||
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Treure silenci de @{name}",
|
"account.unmute": "Treure silenci de @{name}",
|
||||||
"account.unmute_notifications": "Activar notificacions de @{name}",
|
"account.unmute_notifications": "Activar notificacions de @{name}",
|
||||||
"account.view_full_profile": "Mostra el perfil complet",
|
"account.view_full_profile": "Mostra el perfil complet",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "Pots premer {combo} per saltar-te això el proper cop",
|
"boost_modal.combo": "Pots premer {combo} per saltar-te això el proper cop",
|
||||||
"bundle_column_error.body": "S'ha produït un error en carregar aquest component.",
|
"bundle_column_error.body": "S'ha produït un error en carregar aquest component.",
|
||||||
"bundle_column_error.retry": "Torna-ho a provar",
|
"bundle_column_error.retry": "Torna-ho a provar",
|
||||||
|
@ -60,10 +62,10 @@
|
||||||
"compose_form.placeholder": "En què estàs pensant?",
|
"compose_form.placeholder": "En què estàs pensant?",
|
||||||
"compose_form.publish": "Toot",
|
"compose_form.publish": "Toot",
|
||||||
"compose_form.publish_loud": "{publish}!",
|
"compose_form.publish_loud": "{publish}!",
|
||||||
"compose_form.sensitive.marked": "Media is marked as sensitive",
|
"compose_form.sensitive.marked": "Mèdia marcat com a sensible",
|
||||||
"compose_form.sensitive.unmarked": "Media is not marked as sensitive",
|
"compose_form.sensitive.unmarked": "Mèdia no està marcat com a sensible",
|
||||||
"compose_form.spoiler.marked": "Text is hidden behind warning",
|
"compose_form.spoiler.marked": "Text ocult sota l'avís",
|
||||||
"compose_form.spoiler.unmarked": "Text is not hidden",
|
"compose_form.spoiler.unmarked": "Text no ocult",
|
||||||
"compose_form.spoiler_placeholder": "Escriu l'avís aquí",
|
"compose_form.spoiler_placeholder": "Escriu l'avís aquí",
|
||||||
"confirmation_modal.cancel": "Cancel·la",
|
"confirmation_modal.cancel": "Cancel·la",
|
||||||
"confirmations.block.confirm": "Bloca",
|
"confirmations.block.confirm": "Bloca",
|
||||||
|
@ -221,7 +223,7 @@
|
||||||
"report.target": "Informes",
|
"report.target": "Informes",
|
||||||
"search.placeholder": "Cercar",
|
"search.placeholder": "Cercar",
|
||||||
"search_popout.search_format": "Format de cerca avançada",
|
"search_popout.search_format": "Format de cerca avançada",
|
||||||
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
"search_popout.tips.full_text": "Text simple recupera publicacions que has escrit, les marcades com a favorites, les impulsades o en les que has estat esmentat, així com usuaris, noms d'usuari i etiquetes.",
|
||||||
"search_popout.tips.hashtag": "etiqueta",
|
"search_popout.tips.hashtag": "etiqueta",
|
||||||
"search_popout.tips.status": "status",
|
"search_popout.tips.status": "status",
|
||||||
"search_popout.tips.text": "El text simple retorna coincidències amb els noms de visualització, els noms d'usuari i els hashtags",
|
"search_popout.tips.text": "El text simple retorna coincidències amb els noms de visualització, els noms d'usuari i els hashtags",
|
||||||
|
@ -244,7 +246,7 @@
|
||||||
"status.mute_conversation": "Silenciar conversació",
|
"status.mute_conversation": "Silenciar conversació",
|
||||||
"status.open": "Ampliar aquest estat",
|
"status.open": "Ampliar aquest estat",
|
||||||
"status.pin": "Fixat en el perfil",
|
"status.pin": "Fixat en el perfil",
|
||||||
"status.pinned": "Pinned toot",
|
"status.pinned": "Toot fixat",
|
||||||
"status.reblog": "Impuls",
|
"status.reblog": "Impuls",
|
||||||
"status.reblogged_by": "{name} ha retootejat",
|
"status.reblogged_by": "{name} ha retootejat",
|
||||||
"status.reply": "Respondre",
|
"status.reply": "Respondre",
|
||||||
|
@ -254,9 +256,9 @@
|
||||||
"status.sensitive_warning": "Contingut sensible",
|
"status.sensitive_warning": "Contingut sensible",
|
||||||
"status.share": "Compartir",
|
"status.share": "Compartir",
|
||||||
"status.show_less": "Mostra menys",
|
"status.show_less": "Mostra menys",
|
||||||
"status.show_less_all": "Show less for all",
|
"status.show_less_all": "Mostra menys per a tot",
|
||||||
"status.show_more": "Mostra més",
|
"status.show_more": "Mostra més",
|
||||||
"status.show_more_all": "Show more for all",
|
"status.show_more_all": "Mostra més per a tot",
|
||||||
"status.unmute_conversation": "Activar conversació",
|
"status.unmute_conversation": "Activar conversació",
|
||||||
"status.unpin": "Deslliga del perfil",
|
"status.unpin": "Deslliga del perfil",
|
||||||
"tabs_bar.federated_timeline": "Federada",
|
"tabs_bar.federated_timeline": "Federada",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "@{name} nicht mehr stummschalten",
|
"account.unmute": "@{name} nicht mehr stummschalten",
|
||||||
"account.unmute_notifications": "Benachrichtigungen von @{name} einschalten",
|
"account.unmute_notifications": "Benachrichtigungen von @{name} einschalten",
|
||||||
"account.view_full_profile": "Vollständiges Profil anzeigen",
|
"account.view_full_profile": "Vollständiges Profil anzeigen",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "Du kannst {combo} drücken, um dies beim nächsten Mal zu überspringen",
|
"boost_modal.combo": "Du kannst {combo} drücken, um dies beim nächsten Mal zu überspringen",
|
||||||
"bundle_column_error.body": "Etwas ist beim Laden schiefgelaufen.",
|
"bundle_column_error.body": "Etwas ist beim Laden schiefgelaufen.",
|
||||||
"bundle_column_error.retry": "Erneut versuchen",
|
"bundle_column_error.retry": "Erneut versuchen",
|
||||||
|
|
|
@ -326,7 +326,7 @@
|
||||||
"id": "account.posts"
|
"id": "account.posts"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"defaultMessage": "Toots with replies",
|
"defaultMessage": "Toots and replies",
|
||||||
"id": "account.posts_with_replies"
|
"id": "account.posts_with_replies"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1734,5 +1734,18 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/features/video/index.json"
|
"path": "app/javascript/mastodon/features/video/index.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "Oops!",
|
||||||
|
"id": "alert.unexpected.title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "An unexpected error occurred.",
|
||||||
|
"id": "alert.unexpected.message"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"path": "app/javascript/mastodon/middleware/errors.json"
|
||||||
}
|
}
|
||||||
]
|
]
|
|
@ -17,7 +17,7 @@
|
||||||
"account.mute_notifications": "Mute notifications from @{name}",
|
"account.mute_notifications": "Mute notifications from @{name}",
|
||||||
"account.muted": "Muted",
|
"account.muted": "Muted",
|
||||||
"account.posts": "Toots",
|
"account.posts": "Toots",
|
||||||
"account.posts_with_replies": "Toots with replies",
|
"account.posts_with_replies": "Toots and replies",
|
||||||
"account.report": "Report @{name}",
|
"account.report": "Report @{name}",
|
||||||
"account.requested": "Awaiting approval. Click to cancel follow request",
|
"account.requested": "Awaiting approval. Click to cancel follow request",
|
||||||
"account.share": "Share @{name}'s profile",
|
"account.share": "Share @{name}'s profile",
|
||||||
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Unmute @{name}",
|
"account.unmute": "Unmute @{name}",
|
||||||
"account.unmute_notifications": "Unmute notifications from @{name}",
|
"account.unmute_notifications": "Unmute notifications from @{name}",
|
||||||
"account.view_full_profile": "View full profile",
|
"account.view_full_profile": "View full profile",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "You can press {combo} to skip this next time",
|
"boost_modal.combo": "You can press {combo} to skip this next time",
|
||||||
"bundle_column_error.body": "Something went wrong while loading this component.",
|
"bundle_column_error.body": "Something went wrong while loading this component.",
|
||||||
"bundle_column_error.retry": "Try again",
|
"bundle_column_error.retry": "Try again",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Malsilentigi @{name}",
|
"account.unmute": "Malsilentigi @{name}",
|
||||||
"account.unmute_notifications": "Malsilentigi sciigojn de @{name}",
|
"account.unmute_notifications": "Malsilentigi sciigojn de @{name}",
|
||||||
"account.view_full_profile": "Vidi plenan profilon",
|
"account.view_full_profile": "Vidi plenan profilon",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "Vi povas premi {combo} por preterpasi sekvafoje",
|
"boost_modal.combo": "Vi povas premi {combo} por preterpasi sekvafoje",
|
||||||
"bundle_column_error.body": "Io misfunkciis en la ŝargado de ĉi tiu elemento.",
|
"bundle_column_error.body": "Io misfunkciis en la ŝargado de ĉi tiu elemento.",
|
||||||
"bundle_column_error.retry": "Bonvolu reprovi",
|
"bundle_column_error.retry": "Bonvolu reprovi",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Dejar de silenciar a @{name}",
|
"account.unmute": "Dejar de silenciar a @{name}",
|
||||||
"account.unmute_notifications": "Dejar de silenciar las notificaciones de @{name}",
|
"account.unmute_notifications": "Dejar de silenciar las notificaciones de @{name}",
|
||||||
"account.view_full_profile": "Ver perfil completo",
|
"account.view_full_profile": "Ver perfil completo",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "Puedes presionar {combo} para saltear este aviso la próxima vez",
|
"boost_modal.combo": "Puedes presionar {combo} para saltear este aviso la próxima vez",
|
||||||
"bundle_column_error.body": "Algo salió mal al cargar este componente.",
|
"bundle_column_error.body": "Algo salió mal al cargar este componente.",
|
||||||
"bundle_column_error.retry": "Inténtalo de nuevo",
|
"bundle_column_error.retry": "Inténtalo de nuevo",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "باصدا کردن @{name}",
|
"account.unmute": "باصدا کردن @{name}",
|
||||||
"account.unmute_notifications": "باصداکردن اعلانها از طرف @{name}",
|
"account.unmute_notifications": "باصداکردن اعلانها از طرف @{name}",
|
||||||
"account.view_full_profile": "نمایش نمایهٔ کامل",
|
"account.view_full_profile": "نمایش نمایهٔ کامل",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "دکمهٔ {combo} را بزنید تا دیگر این را نبینید",
|
"boost_modal.combo": "دکمهٔ {combo} را بزنید تا دیگر این را نبینید",
|
||||||
"bundle_column_error.body": "هنگام بازکردن این بخش خطایی رخ داد.",
|
"bundle_column_error.body": "هنگام بازکردن این بخش خطایی رخ داد.",
|
||||||
"bundle_column_error.retry": "تلاش دوباره",
|
"bundle_column_error.retry": "تلاش دوباره",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Poista mykistys käyttäjältä @{name}",
|
"account.unmute": "Poista mykistys käyttäjältä @{name}",
|
||||||
"account.unmute_notifications": "Poista mykistys käyttäjän @{name} ilmoituksilta",
|
"account.unmute_notifications": "Poista mykistys käyttäjän @{name} ilmoituksilta",
|
||||||
"account.view_full_profile": "Näytä koko profiili",
|
"account.view_full_profile": "Näytä koko profiili",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "Voit painaa näppäimiä {combo} ohittaaksesi tämän ensi kerralla",
|
"boost_modal.combo": "Voit painaa näppäimiä {combo} ohittaaksesi tämän ensi kerralla",
|
||||||
"bundle_column_error.body": "Jokin meni vikaan tätä komponenttia ladatessa.",
|
"bundle_column_error.body": "Jokin meni vikaan tätä komponenttia ladatessa.",
|
||||||
"bundle_column_error.retry": "Yritä uudestaan",
|
"bundle_column_error.retry": "Yritä uudestaan",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Ne plus masquer",
|
"account.unmute": "Ne plus masquer",
|
||||||
"account.unmute_notifications": "Réactiver les notifications de @{name}",
|
"account.unmute_notifications": "Réactiver les notifications de @{name}",
|
||||||
"account.view_full_profile": "Afficher le profil complet",
|
"account.view_full_profile": "Afficher le profil complet",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois",
|
"boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois",
|
||||||
"bundle_column_error.body": "Une erreur s’est produite lors du chargement de ce composant.",
|
"bundle_column_error.body": "Une erreur s’est produite lors du chargement de ce composant.",
|
||||||
"bundle_column_error.retry": "Réessayer",
|
"bundle_column_error.retry": "Réessayer",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Non acalar @{name}",
|
"account.unmute": "Non acalar @{name}",
|
||||||
"account.unmute_notifications": "Desbloquear as notificacións de @{name}",
|
"account.unmute_notifications": "Desbloquear as notificacións de @{name}",
|
||||||
"account.view_full_profile": "Ver o perfil completo",
|
"account.view_full_profile": "Ver o perfil completo",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "Pulse {combo} para saltar esto a próxima vez",
|
"boost_modal.combo": "Pulse {combo} para saltar esto a próxima vez",
|
||||||
"bundle_column_error.body": "Houbo un fallo mentras se cargaba este compoñente.",
|
"bundle_column_error.body": "Houbo un fallo mentras se cargaba este compoñente.",
|
||||||
"bundle_column_error.retry": "Inténteo de novo",
|
"bundle_column_error.retry": "Inténteo de novo",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "הפסקת השתקת @{name}",
|
"account.unmute": "הפסקת השתקת @{name}",
|
||||||
"account.unmute_notifications": "להפסיק הסתרת הודעות מעם @{name}",
|
"account.unmute_notifications": "להפסיק הסתרת הודעות מעם @{name}",
|
||||||
"account.view_full_profile": "הראה אודות מלאות",
|
"account.view_full_profile": "הראה אודות מלאות",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "ניתן להקיש {combo} כדי לדלג בפעם הבאה",
|
"boost_modal.combo": "ניתן להקיש {combo} כדי לדלג בפעם הבאה",
|
||||||
"bundle_column_error.body": "משהו השתבש בעת הצגת הרכיב הזה.",
|
"bundle_column_error.body": "משהו השתבש בעת הצגת הרכיב הזה.",
|
||||||
"bundle_column_error.retry": "לנסות שוב",
|
"bundle_column_error.retry": "לנסות שוב",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Poništi utišavanje @{name}",
|
"account.unmute": "Poništi utišavanje @{name}",
|
||||||
"account.unmute_notifications": "Unmute notifications from @{name}",
|
"account.unmute_notifications": "Unmute notifications from @{name}",
|
||||||
"account.view_full_profile": "View full profile",
|
"account.view_full_profile": "View full profile",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "Možeš pritisnuti {combo} kako bi ovo preskočio sljedeći put",
|
"boost_modal.combo": "Možeš pritisnuti {combo} kako bi ovo preskočio sljedeći put",
|
||||||
"bundle_column_error.body": "Something went wrong while loading this component.",
|
"bundle_column_error.body": "Something went wrong while loading this component.",
|
||||||
"bundle_column_error.retry": "Try again",
|
"bundle_column_error.retry": "Try again",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "@{name} kinémítása",
|
"account.unmute": "@{name} kinémítása",
|
||||||
"account.unmute_notifications": "@{name} értesítéseinek kinémítása",
|
"account.unmute_notifications": "@{name} értesítéseinek kinémítása",
|
||||||
"account.view_full_profile": "Teljes profil megtekintése",
|
"account.view_full_profile": "Teljes profil megtekintése",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "Megnyomhatod {combo}, hogy átugord következő alkalommal",
|
"boost_modal.combo": "Megnyomhatod {combo}, hogy átugord következő alkalommal",
|
||||||
"bundle_column_error.body": "Hiba történt a komponens betöltése közben.",
|
"bundle_column_error.body": "Hiba történt a komponens betöltése közben.",
|
||||||
"bundle_column_error.retry": "Próbálja újra",
|
"bundle_column_error.retry": "Próbálja újra",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Ապալռեցնել @{name}֊ին",
|
"account.unmute": "Ապալռեցնել @{name}֊ին",
|
||||||
"account.unmute_notifications": "Միացնել ծանուցումները @{name}֊ից",
|
"account.unmute_notifications": "Միացնել ծանուցումները @{name}֊ից",
|
||||||
"account.view_full_profile": "Դիտել ամբողջական տարբերակը։",
|
"account.view_full_profile": "Դիտել ամբողջական տարբերակը։",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "Կարող ես սեղմել {combo}՝ սա հաջորդ անգամ բաց թողնելու համար",
|
"boost_modal.combo": "Կարող ես սեղմել {combo}՝ սա հաջորդ անգամ բաց թողնելու համար",
|
||||||
"bundle_column_error.body": "Այս բաղադրիչը բեռնելու ընթացքում ինչ֊որ բան խափանվեց։",
|
"bundle_column_error.body": "Այս բաղադրիչը բեռնելու ընթացքում ինչ֊որ բան խափանվեց։",
|
||||||
"bundle_column_error.retry": "Կրկին փորձել",
|
"bundle_column_error.retry": "Կրկին փորձել",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Berhenti membisukan @{name}",
|
"account.unmute": "Berhenti membisukan @{name}",
|
||||||
"account.unmute_notifications": "Munculkan notifikasi dari @{name}",
|
"account.unmute_notifications": "Munculkan notifikasi dari @{name}",
|
||||||
"account.view_full_profile": "Lihat profil lengkap",
|
"account.view_full_profile": "Lihat profil lengkap",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "Anda dapat menekan {combo} untuk melewati ini",
|
"boost_modal.combo": "Anda dapat menekan {combo} untuk melewati ini",
|
||||||
"bundle_column_error.body": "Kesalahan terjadi saat memuat komponen ini.",
|
"bundle_column_error.body": "Kesalahan terjadi saat memuat komponen ini.",
|
||||||
"bundle_column_error.retry": "Coba lagi",
|
"bundle_column_error.retry": "Coba lagi",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Ne plus celar @{name}",
|
"account.unmute": "Ne plus celar @{name}",
|
||||||
"account.unmute_notifications": "Unmute notifications from @{name}",
|
"account.unmute_notifications": "Unmute notifications from @{name}",
|
||||||
"account.view_full_profile": "View full profile",
|
"account.view_full_profile": "View full profile",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "Tu povas presar sur {combo} por omisar co en la venonta foyo",
|
"boost_modal.combo": "Tu povas presar sur {combo} por omisar co en la venonta foyo",
|
||||||
"bundle_column_error.body": "Something went wrong while loading this component.",
|
"bundle_column_error.body": "Something went wrong while loading this component.",
|
||||||
"bundle_column_error.retry": "Try again",
|
"bundle_column_error.retry": "Try again",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Non silenziare @{name}",
|
"account.unmute": "Non silenziare @{name}",
|
||||||
"account.unmute_notifications": "Unmute notifications from @{name}",
|
"account.unmute_notifications": "Unmute notifications from @{name}",
|
||||||
"account.view_full_profile": "View full profile",
|
"account.view_full_profile": "View full profile",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "Puoi premere {combo} per saltare questo passaggio la prossima volta",
|
"boost_modal.combo": "Puoi premere {combo} per saltare questo passaggio la prossima volta",
|
||||||
"bundle_column_error.body": "Something went wrong while loading this component.",
|
"bundle_column_error.body": "Something went wrong while loading this component.",
|
||||||
"bundle_column_error.retry": "Try again",
|
"bundle_column_error.retry": "Try again",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "@{name}さんのミュートを解除",
|
"account.unmute": "@{name}さんのミュートを解除",
|
||||||
"account.unmute_notifications": "@{name}さんからの通知を受け取る",
|
"account.unmute_notifications": "@{name}さんからの通知を受け取る",
|
||||||
"account.view_full_profile": "全ての情報を見る",
|
"account.view_full_profile": "全ての情報を見る",
|
||||||
|
"alert.unexpected.message": "不明なエラーが発生しました",
|
||||||
|
"alert.unexpected.title": "エラー",
|
||||||
"boost_modal.combo": "次からは{combo}を押せばスキップできます",
|
"boost_modal.combo": "次からは{combo}を押せばスキップできます",
|
||||||
"bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。",
|
"bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。",
|
||||||
"bundle_column_error.retry": "再試行",
|
"bundle_column_error.retry": "再試行",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "뮤트 해제",
|
"account.unmute": "뮤트 해제",
|
||||||
"account.unmute_notifications": "@{name}의 알림 뮤트 해제",
|
"account.unmute_notifications": "@{name}의 알림 뮤트 해제",
|
||||||
"account.view_full_profile": "전체 프로필 보기",
|
"account.view_full_profile": "전체 프로필 보기",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "다음부터 {combo}를 누르면 이 과정을 건너뛸 수 있습니다.",
|
"boost_modal.combo": "다음부터 {combo}를 누르면 이 과정을 건너뛸 수 있습니다.",
|
||||||
"bundle_column_error.body": "컴포넌트를 불러오는 과정에서 문제가 발생했습니다.",
|
"bundle_column_error.body": "컴포넌트를 불러오는 과정에서 문제가 발생했습니다.",
|
||||||
"bundle_column_error.retry": "다시 시도",
|
"bundle_column_error.retry": "다시 시도",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "@{name} niet meer negeren",
|
"account.unmute": "@{name} niet meer negeren",
|
||||||
"account.unmute_notifications": "@{name} meldingen niet meer negeren",
|
"account.unmute_notifications": "@{name} meldingen niet meer negeren",
|
||||||
"account.view_full_profile": "Volledig profiel tonen",
|
"account.view_full_profile": "Volledig profiel tonen",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "Je kunt {combo} klikken om dit de volgende keer over te slaan",
|
"boost_modal.combo": "Je kunt {combo} klikken om dit de volgende keer over te slaan",
|
||||||
"bundle_column_error.body": "Tijdens het laden van dit onderdeel is er iets fout gegaan.",
|
"bundle_column_error.body": "Tijdens het laden van dit onderdeel is er iets fout gegaan.",
|
||||||
"bundle_column_error.retry": "Opnieuw proberen",
|
"bundle_column_error.retry": "Opnieuw proberen",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Avdemp @{name}",
|
"account.unmute": "Avdemp @{name}",
|
||||||
"account.unmute_notifications": "Vis varsler fra @{name}",
|
"account.unmute_notifications": "Vis varsler fra @{name}",
|
||||||
"account.view_full_profile": "Vis hele profilen",
|
"account.view_full_profile": "Vis hele profilen",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "You kan trykke {combo} for å hoppe over dette neste gang",
|
"boost_modal.combo": "You kan trykke {combo} for å hoppe over dette neste gang",
|
||||||
"bundle_column_error.body": "Noe gikk galt mens denne komponenten lastet.",
|
"bundle_column_error.body": "Noe gikk galt mens denne komponenten lastet.",
|
||||||
"bundle_column_error.retry": "Prøv igjen",
|
"bundle_column_error.retry": "Prøv igjen",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Quitar de rescondre @{name}",
|
"account.unmute": "Quitar de rescondre @{name}",
|
||||||
"account.unmute_notifications": "Mostrar las notificacions de @{name}",
|
"account.unmute_notifications": "Mostrar las notificacions de @{name}",
|
||||||
"account.view_full_profile": "Veire lo perfil complèt",
|
"account.view_full_profile": "Veire lo perfil complèt",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "Podètz botar {combo} per passar aquò lo còp que ven",
|
"boost_modal.combo": "Podètz botar {combo} per passar aquò lo còp que ven",
|
||||||
"bundle_column_error.body": "Quicòm a fach mèuca pendent lo cargament d’aqueste compausant.",
|
"bundle_column_error.body": "Quicòm a fach mèuca pendent lo cargament d’aqueste compausant.",
|
||||||
"bundle_column_error.retry": "Tornar ensajar",
|
"bundle_column_error.retry": "Tornar ensajar",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Cofnij wyciszenie @{name}",
|
"account.unmute": "Cofnij wyciszenie @{name}",
|
||||||
"account.unmute_notifications": "Cofnij wyciszenie powiadomień od @{name}",
|
"account.unmute_notifications": "Cofnij wyciszenie powiadomień od @{name}",
|
||||||
"account.view_full_profile": "Wyświetl pełny profil",
|
"account.view_full_profile": "Wyświetl pełny profil",
|
||||||
|
"alert.unexpected.message": "Wystąpił nieoczekiwany błąd.",
|
||||||
|
"alert.unexpected.title": "O nie!",
|
||||||
"boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem",
|
"boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem",
|
||||||
"bundle_column_error.body": "Coś poszło nie tak podczas ładowania tego składnika.",
|
"bundle_column_error.body": "Coś poszło nie tak podczas ładowania tego składnika.",
|
||||||
"bundle_column_error.retry": "Spróbuj ponownie",
|
"bundle_column_error.retry": "Spróbuj ponownie",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Não silenciar @{name}",
|
"account.unmute": "Não silenciar @{name}",
|
||||||
"account.unmute_notifications": "Retirar silêncio das notificações vindas de @{name}",
|
"account.unmute_notifications": "Retirar silêncio das notificações vindas de @{name}",
|
||||||
"account.view_full_profile": "Ver perfil completo",
|
"account.view_full_profile": "Ver perfil completo",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "Você pode pressionar {combo} para ignorar este diálogo na próxima vez",
|
"boost_modal.combo": "Você pode pressionar {combo} para ignorar este diálogo na próxima vez",
|
||||||
"bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.",
|
"bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.",
|
||||||
"bundle_column_error.retry": "Tente novamente",
|
"bundle_column_error.retry": "Tente novamente",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Não silenciar @{name}",
|
"account.unmute": "Não silenciar @{name}",
|
||||||
"account.unmute_notifications": "Deixar de silenciar @{name}",
|
"account.unmute_notifications": "Deixar de silenciar @{name}",
|
||||||
"account.view_full_profile": "Ver perfil completo",
|
"account.view_full_profile": "Ver perfil completo",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "Pode clicar {combo} para não voltar a ver",
|
"boost_modal.combo": "Pode clicar {combo} para não voltar a ver",
|
||||||
"bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.",
|
"bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.",
|
||||||
"bundle_column_error.retry": "Tente de novo",
|
"bundle_column_error.retry": "Tente de novo",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Снять глушение",
|
"account.unmute": "Снять глушение",
|
||||||
"account.unmute_notifications": "Показывать уведомления от @{name}",
|
"account.unmute_notifications": "Показывать уведомления от @{name}",
|
||||||
"account.view_full_profile": "Показать полный профиль",
|
"account.view_full_profile": "Показать полный профиль",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз",
|
"boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз",
|
||||||
"bundle_column_error.body": "Что-то пошло не так при загрузке этого компонента.",
|
"bundle_column_error.body": "Что-то пошло не так при загрузке этого компонента.",
|
||||||
"bundle_column_error.retry": "Попробовать снова",
|
"bundle_column_error.retry": "Попробовать снова",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Prestať ignorovať @{name}",
|
"account.unmute": "Prestať ignorovať @{name}",
|
||||||
"account.unmute_notifications": "Odtĺmiť notifikácie od @{name}",
|
"account.unmute_notifications": "Odtĺmiť notifikácie od @{name}",
|
||||||
"account.view_full_profile": "Pozri celý profil",
|
"account.view_full_profile": "Pozri celý profil",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "Nabudúce môžete kliknúť {combo} aby ste preskočili",
|
"boost_modal.combo": "Nabudúce môžete kliknúť {combo} aby ste preskočili",
|
||||||
"bundle_column_error.body": "Nastala chyba pri načítaní tohto komponentu.",
|
"bundle_column_error.body": "Nastala chyba pri načítaní tohto komponentu.",
|
||||||
"bundle_column_error.retry": "Skúste znova",
|
"bundle_column_error.retry": "Skúste znova",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Ukloni ućutkavanje korisniku @{name}",
|
"account.unmute": "Ukloni ućutkavanje korisniku @{name}",
|
||||||
"account.unmute_notifications": "Uključi nazad obaveštenja od korisnika @{name}",
|
"account.unmute_notifications": "Uključi nazad obaveštenja od korisnika @{name}",
|
||||||
"account.view_full_profile": "Vidi ceo profil",
|
"account.view_full_profile": "Vidi ceo profil",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "Možete pritisnuti {combo} da preskočite ovo sledeći put",
|
"boost_modal.combo": "Možete pritisnuti {combo} da preskočite ovo sledeći put",
|
||||||
"bundle_column_error.body": "Nešto je pošlo po zlu prilikom učitavanja ove komponente.",
|
"bundle_column_error.body": "Nešto je pošlo po zlu prilikom učitavanja ove komponente.",
|
||||||
"bundle_column_error.retry": "Pokušajte ponovo",
|
"bundle_column_error.retry": "Pokušajte ponovo",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Уклони ућуткавање кориснику @{name}",
|
"account.unmute": "Уклони ућуткавање кориснику @{name}",
|
||||||
"account.unmute_notifications": "Укључи назад обавештења од корисника @{name}",
|
"account.unmute_notifications": "Укључи назад обавештења од корисника @{name}",
|
||||||
"account.view_full_profile": "Види цео профил",
|
"account.view_full_profile": "Види цео профил",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "Можете притиснути {combo} да прескочите ово следећи пут",
|
"boost_modal.combo": "Можете притиснути {combo} да прескочите ово следећи пут",
|
||||||
"bundle_column_error.body": "Нешто је пошло по злу приликом учитавања ове компоненте.",
|
"bundle_column_error.body": "Нешто је пошло по злу приликом учитавања ове компоненте.",
|
||||||
"bundle_column_error.retry": "Покушајте поново",
|
"bundle_column_error.retry": "Покушајте поново",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Ta bort tystad @{name}",
|
"account.unmute": "Ta bort tystad @{name}",
|
||||||
"account.unmute_notifications": "Återaktivera notifikationer från @{name}",
|
"account.unmute_notifications": "Återaktivera notifikationer från @{name}",
|
||||||
"account.view_full_profile": "Visa hela profilen",
|
"account.view_full_profile": "Visa hela profilen",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "Du kan trycka {combo} för att slippa denna nästa gång",
|
"boost_modal.combo": "Du kan trycka {combo} för att slippa denna nästa gång",
|
||||||
"bundle_column_error.body": "Något gick fel när du laddade denna komponent.",
|
"bundle_column_error.body": "Något gick fel när du laddade denna komponent.",
|
||||||
"bundle_column_error.retry": "Försök igen",
|
"bundle_column_error.retry": "Försök igen",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Unmute @{name}",
|
"account.unmute": "Unmute @{name}",
|
||||||
"account.unmute_notifications": "Unmute notifications from @{name}",
|
"account.unmute_notifications": "Unmute notifications from @{name}",
|
||||||
"account.view_full_profile": "View full profile",
|
"account.view_full_profile": "View full profile",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "You can press {combo} to skip this next time",
|
"boost_modal.combo": "You can press {combo} to skip this next time",
|
||||||
"bundle_column_error.body": "Something went wrong while loading this component.",
|
"bundle_column_error.body": "Something went wrong while loading this component.",
|
||||||
"bundle_column_error.retry": "Try again",
|
"bundle_column_error.retry": "Try again",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Sesi aç @{name}",
|
"account.unmute": "Sesi aç @{name}",
|
||||||
"account.unmute_notifications": "Unmute notifications from @{name}",
|
"account.unmute_notifications": "Unmute notifications from @{name}",
|
||||||
"account.view_full_profile": "View full profile",
|
"account.view_full_profile": "View full profile",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "Bir dahaki sefere {combo} tuşuna basabilirsiniz",
|
"boost_modal.combo": "Bir dahaki sefere {combo} tuşuna basabilirsiniz",
|
||||||
"bundle_column_error.body": "Something went wrong while loading this component.",
|
"bundle_column_error.body": "Something went wrong while loading this component.",
|
||||||
"bundle_column_error.retry": "Try again",
|
"bundle_column_error.retry": "Try again",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "Зняти глушення",
|
"account.unmute": "Зняти глушення",
|
||||||
"account.unmute_notifications": "Unmute notifications from @{name}",
|
"account.unmute_notifications": "Unmute notifications from @{name}",
|
||||||
"account.view_full_profile": "View full profile",
|
"account.view_full_profile": "View full profile",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "Ви можете натиснути {combo}, щоб пропустити це наступного разу",
|
"boost_modal.combo": "Ви можете натиснути {combo}, щоб пропустити це наступного разу",
|
||||||
"bundle_column_error.body": "Something went wrong while loading this component.",
|
"bundle_column_error.body": "Something went wrong while loading this component.",
|
||||||
"bundle_column_error.retry": "Try again",
|
"bundle_column_error.retry": "Try again",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "不再隐藏 @{name}",
|
"account.unmute": "不再隐藏 @{name}",
|
||||||
"account.unmute_notifications": "不再隐藏来自 @{name} 的通知",
|
"account.unmute_notifications": "不再隐藏来自 @{name} 的通知",
|
||||||
"account.view_full_profile": "查看完整资料",
|
"account.view_full_profile": "查看完整资料",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "下次按住 {combo} 即可跳过此提示",
|
"boost_modal.combo": "下次按住 {combo} 即可跳过此提示",
|
||||||
"bundle_column_error.body": "载入这个组件时发生了错误。",
|
"bundle_column_error.body": "载入这个组件时发生了错误。",
|
||||||
"bundle_column_error.retry": "重试",
|
"bundle_column_error.retry": "重试",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "取消 @{name} 的靜音",
|
"account.unmute": "取消 @{name} 的靜音",
|
||||||
"account.unmute_notifications": "Unmute notifications from @{name}",
|
"account.unmute_notifications": "Unmute notifications from @{name}",
|
||||||
"account.view_full_profile": "查看完整資料",
|
"account.view_full_profile": "查看完整資料",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "如你想在下次路過這顯示,請按{combo},",
|
"boost_modal.combo": "如你想在下次路過這顯示,請按{combo},",
|
||||||
"bundle_column_error.body": "加載本組件出錯。",
|
"bundle_column_error.body": "加載本組件出錯。",
|
||||||
"bundle_column_error.retry": "重試",
|
"bundle_column_error.retry": "重試",
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"account.unmute": "不再消音 @{name}",
|
"account.unmute": "不再消音 @{name}",
|
||||||
"account.unmute_notifications": "Unmute notifications from @{name}",
|
"account.unmute_notifications": "Unmute notifications from @{name}",
|
||||||
"account.view_full_profile": "查看完整資訊",
|
"account.view_full_profile": "查看完整資訊",
|
||||||
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
"alert.unexpected.title": "Oops!",
|
||||||
"boost_modal.combo": "下次你可以按 {combo} 來跳過",
|
"boost_modal.combo": "下次你可以按 {combo} 來跳過",
|
||||||
"bundle_column_error.body": "加載本組件出錯。",
|
"bundle_column_error.body": "加載本組件出錯。",
|
||||||
"bundle_column_error.retry": "重試",
|
"bundle_column_error.retry": "重試",
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
|
import { defineMessages } from 'react-intl';
|
||||||
import { showAlert } from '../actions/alerts';
|
import { showAlert } from '../actions/alerts';
|
||||||
|
|
||||||
const defaultFailSuffix = 'FAIL';
|
const defaultFailSuffix = 'FAIL';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
|
||||||
|
unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' },
|
||||||
|
});
|
||||||
|
|
||||||
export default function errorsMiddleware() {
|
export default function errorsMiddleware() {
|
||||||
return ({ dispatch }) => next => action => {
|
return ({ dispatch }) => next => action => {
|
||||||
if (action.type && !action.skipAlert) {
|
if (action.type && !action.skipAlert) {
|
||||||
|
@ -21,7 +27,7 @@ export default function errorsMiddleware() {
|
||||||
dispatch(showAlert(title, message));
|
dispatch(showAlert(title, message));
|
||||||
} else {
|
} else {
|
||||||
console.error(action.error);
|
console.error(action.error);
|
||||||
dispatch(showAlert('Oops!', 'An unexpected error occurred.'));
|
dispatch(showAlert(messages.unexpectedTitle, messages.unexpectedMessage));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,56 +1,7 @@
|
||||||
import {
|
import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer';
|
||||||
ACCOUNT_FETCH_SUCCESS,
|
|
||||||
FOLLOWERS_FETCH_SUCCESS,
|
|
||||||
FOLLOWERS_EXPAND_SUCCESS,
|
|
||||||
FOLLOWING_FETCH_SUCCESS,
|
|
||||||
FOLLOWING_EXPAND_SUCCESS,
|
|
||||||
FOLLOW_REQUESTS_FETCH_SUCCESS,
|
|
||||||
FOLLOW_REQUESTS_EXPAND_SUCCESS,
|
|
||||||
} from '../actions/accounts';
|
|
||||||
import {
|
|
||||||
BLOCKS_FETCH_SUCCESS,
|
|
||||||
BLOCKS_EXPAND_SUCCESS,
|
|
||||||
} from '../actions/blocks';
|
|
||||||
import {
|
|
||||||
MUTES_FETCH_SUCCESS,
|
|
||||||
MUTES_EXPAND_SUCCESS,
|
|
||||||
} from '../actions/mutes';
|
|
||||||
import { COMPOSE_SUGGESTIONS_READY } from '../actions/compose';
|
|
||||||
import {
|
|
||||||
REBLOG_SUCCESS,
|
|
||||||
UNREBLOG_SUCCESS,
|
|
||||||
FAVOURITE_SUCCESS,
|
|
||||||
UNFAVOURITE_SUCCESS,
|
|
||||||
REBLOGS_FETCH_SUCCESS,
|
|
||||||
FAVOURITES_FETCH_SUCCESS,
|
|
||||||
} from '../actions/interactions';
|
|
||||||
import {
|
|
||||||
TIMELINE_REFRESH_SUCCESS,
|
|
||||||
TIMELINE_UPDATE,
|
|
||||||
TIMELINE_EXPAND_SUCCESS,
|
|
||||||
} from '../actions/timelines';
|
|
||||||
import {
|
|
||||||
STATUS_FETCH_SUCCESS,
|
|
||||||
CONTEXT_FETCH_SUCCESS,
|
|
||||||
} from '../actions/statuses';
|
|
||||||
import { SEARCH_FETCH_SUCCESS } from '../actions/search';
|
|
||||||
import {
|
|
||||||
NOTIFICATIONS_UPDATE,
|
|
||||||
NOTIFICATIONS_REFRESH_SUCCESS,
|
|
||||||
NOTIFICATIONS_EXPAND_SUCCESS,
|
|
||||||
} from '../actions/notifications';
|
|
||||||
import {
|
|
||||||
FAVOURITED_STATUSES_FETCH_SUCCESS,
|
|
||||||
FAVOURITED_STATUSES_EXPAND_SUCCESS,
|
|
||||||
} from '../actions/favourites';
|
|
||||||
import {
|
|
||||||
LIST_ACCOUNTS_FETCH_SUCCESS,
|
|
||||||
LIST_EDITOR_SUGGESTIONS_READY,
|
|
||||||
} from '../actions/lists';
|
|
||||||
import { STORE_HYDRATE } from '../actions/store';
|
|
||||||
import emojify from '../features/emoji/emoji';
|
|
||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||||
import escapeTextContentForBrowser from 'escape-html';
|
|
||||||
|
const initialState = ImmutableMap();
|
||||||
|
|
||||||
const normalizeAccount = (state, account) => {
|
const normalizeAccount = (state, account) => {
|
||||||
account = { ...account };
|
account = { ...account };
|
||||||
|
@ -59,15 +10,6 @@ const normalizeAccount = (state, account) => {
|
||||||
delete account.following_count;
|
delete account.following_count;
|
||||||
delete account.statuses_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);
|
|
||||||
|
|
||||||
if (account.moved) {
|
|
||||||
state = normalizeAccount(state, account.moved);
|
|
||||||
account.moved = account.moved.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return state.set(account.id, fromJS(account));
|
return state.set(account.id, fromJS(account));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -79,67 +21,12 @@ const normalizeAccounts = (state, accounts) => {
|
||||||
return state;
|
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) {
|
export default function accounts(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case STORE_HYDRATE:
|
case ACCOUNT_IMPORT:
|
||||||
return normalizeAccounts(state, Object.values(action.state.get('accounts').toJS()));
|
|
||||||
case ACCOUNT_FETCH_SUCCESS:
|
|
||||||
case NOTIFICATIONS_UPDATE:
|
|
||||||
return normalizeAccount(state, action.account);
|
return normalizeAccount(state, action.account);
|
||||||
case FOLLOWERS_FETCH_SUCCESS:
|
case ACCOUNTS_IMPORT:
|
||||||
case FOLLOWERS_EXPAND_SUCCESS:
|
return normalizeAccounts(state, action.accounts);
|
||||||
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:
|
|
||||||
case LIST_ACCOUNTS_FETCH_SUCCESS:
|
|
||||||
case LIST_EDITOR_SUGGESTIONS_READY:
|
|
||||||
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:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,55 +1,8 @@
|
||||||
import {
|
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_FOLLOW_SUCCESS,
|
||||||
ACCOUNT_UNFOLLOW_SUCCESS,
|
ACCOUNT_UNFOLLOW_SUCCESS,
|
||||||
} from '../actions/accounts';
|
} from '../actions/accounts';
|
||||||
import {
|
import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer';
|
||||||
BLOCKS_FETCH_SUCCESS,
|
|
||||||
BLOCKS_EXPAND_SUCCESS,
|
|
||||||
} from '../actions/blocks';
|
|
||||||
import {
|
|
||||||
MUTES_FETCH_SUCCESS,
|
|
||||||
MUTES_EXPAND_SUCCESS,
|
|
||||||
} from '../actions/mutes';
|
|
||||||
import { COMPOSE_SUGGESTIONS_READY } from '../actions/compose';
|
|
||||||
import {
|
|
||||||
REBLOG_SUCCESS,
|
|
||||||
UNREBLOG_SUCCESS,
|
|
||||||
FAVOURITE_SUCCESS,
|
|
||||||
UNFAVOURITE_SUCCESS,
|
|
||||||
REBLOGS_FETCH_SUCCESS,
|
|
||||||
FAVOURITES_FETCH_SUCCESS,
|
|
||||||
} from '../actions/interactions';
|
|
||||||
import {
|
|
||||||
TIMELINE_REFRESH_SUCCESS,
|
|
||||||
TIMELINE_UPDATE,
|
|
||||||
TIMELINE_EXPAND_SUCCESS,
|
|
||||||
} from '../actions/timelines';
|
|
||||||
import {
|
|
||||||
STATUS_FETCH_SUCCESS,
|
|
||||||
CONTEXT_FETCH_SUCCESS,
|
|
||||||
} from '../actions/statuses';
|
|
||||||
import { SEARCH_FETCH_SUCCESS } from '../actions/search';
|
|
||||||
import {
|
|
||||||
NOTIFICATIONS_UPDATE,
|
|
||||||
NOTIFICATIONS_REFRESH_SUCCESS,
|
|
||||||
NOTIFICATIONS_EXPAND_SUCCESS,
|
|
||||||
} from '../actions/notifications';
|
|
||||||
import {
|
|
||||||
FAVOURITED_STATUSES_FETCH_SUCCESS,
|
|
||||||
FAVOURITED_STATUSES_EXPAND_SUCCESS,
|
|
||||||
} from '../actions/favourites';
|
|
||||||
import {
|
|
||||||
LIST_ACCOUNTS_FETCH_SUCCESS,
|
|
||||||
LIST_EDITOR_SUGGESTIONS_READY,
|
|
||||||
} from '../actions/lists';
|
|
||||||
import { STORE_HYDRATE } from '../actions/store';
|
|
||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||||
|
|
||||||
const normalizeAccount = (state, account) => state.set(account.id, fromJS({
|
const normalizeAccount = (state, account) => state.set(account.id, fromJS({
|
||||||
|
@ -66,71 +19,14 @@ const normalizeAccounts = (state, accounts) => {
|
||||||
return state;
|
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();
|
const initialState = ImmutableMap();
|
||||||
|
|
||||||
export default function accountsCounters(state = initialState, action) {
|
export default function accountsCounters(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case STORE_HYDRATE:
|
case ACCOUNT_IMPORT:
|
||||||
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);
|
return normalizeAccount(state, action.account);
|
||||||
case FOLLOWERS_FETCH_SUCCESS:
|
case ACCOUNTS_IMPORT:
|
||||||
case FOLLOWERS_EXPAND_SUCCESS:
|
return normalizeAccounts(state, action.accounts);
|
||||||
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:
|
|
||||||
case LIST_ACCOUNTS_FETCH_SUCCESS:
|
|
||||||
case LIST_EDITOR_SUGGESTIONS_READY:
|
|
||||||
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:
|
case ACCOUNT_FOLLOW_SUCCESS:
|
||||||
return action.alreadyFollowing ? state :
|
return action.alreadyFollowing ? state :
|
||||||
state.updateIn([action.relationship.id, 'followers_count'], num => num + 1);
|
state.updateIn([action.relationship.id, 'followers_count'], num => num + 1);
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
import {
|
import {
|
||||||
NOTIFICATIONS_UPDATE,
|
NOTIFICATIONS_UPDATE,
|
||||||
NOTIFICATIONS_REFRESH_SUCCESS,
|
|
||||||
NOTIFICATIONS_EXPAND_SUCCESS,
|
NOTIFICATIONS_EXPAND_SUCCESS,
|
||||||
NOTIFICATIONS_REFRESH_REQUEST,
|
|
||||||
NOTIFICATIONS_EXPAND_REQUEST,
|
NOTIFICATIONS_EXPAND_REQUEST,
|
||||||
NOTIFICATIONS_REFRESH_FAIL,
|
|
||||||
NOTIFICATIONS_EXPAND_FAIL,
|
NOTIFICATIONS_EXPAND_FAIL,
|
||||||
NOTIFICATIONS_CLEAR,
|
NOTIFICATIONS_CLEAR,
|
||||||
NOTIFICATIONS_SCROLL_TOP,
|
NOTIFICATIONS_SCROLL_TOP,
|
||||||
|
@ -13,16 +10,15 @@ import {
|
||||||
ACCOUNT_BLOCK_SUCCESS,
|
ACCOUNT_BLOCK_SUCCESS,
|
||||||
ACCOUNT_MUTE_SUCCESS,
|
ACCOUNT_MUTE_SUCCESS,
|
||||||
} from '../actions/accounts';
|
} from '../actions/accounts';
|
||||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
|
||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
items: ImmutableList(),
|
items: ImmutableList(),
|
||||||
next: null,
|
hasMore: true,
|
||||||
top: true,
|
top: true,
|
||||||
unread: 0,
|
unread: 0,
|
||||||
loaded: false,
|
isLoading: false,
|
||||||
isLoading: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const notificationToMap = notification => ImmutableMap({
|
const notificationToMap = notification => ImmutableMap({
|
||||||
|
@ -48,35 +44,41 @@ const normalizeNotification = (state, notification) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeNotifications = (state, notifications, next) => {
|
const newer = (m, n) => {
|
||||||
let items = ImmutableList();
|
const mId = m.get('id');
|
||||||
const loaded = state.get('loaded');
|
const nId = n.get('id');
|
||||||
|
|
||||||
notifications.forEach((n, i) => {
|
return mId.length === nId.length ? mId > nId : mId.length > nId.length;
|
||||||
items = items.set(i, notificationToMap(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) => {
|
const expandNormalizedNotifications = (state, notifications, next) => {
|
||||||
let items = ImmutableList();
|
let items = ImmutableList();
|
||||||
|
|
||||||
notifications.forEach((n, i) => {
|
notifications.forEach((n, i) => {
|
||||||
items = items.set(i, notificationToMap(n));
|
items = items.set(i, notificationToMap(n));
|
||||||
});
|
});
|
||||||
|
|
||||||
return state
|
return state.withMutations(mutable => {
|
||||||
.update('items', list => list.concat(items))
|
if (!items.isEmpty()) {
|
||||||
.set('next', next)
|
mutable.update('items', list => {
|
||||||
.set('isLoading', false);
|
const lastIndex = 1 + list.findLastIndex(
|
||||||
|
item => item !== null && (newer(item, items.last()) || item.get('id') === items.last().get('id'))
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstIndex = 1 + list.take(lastIndex).findLastIndex(
|
||||||
|
item => item !== null && newer(item, items.first())
|
||||||
|
);
|
||||||
|
|
||||||
|
return list.take(firstIndex).concat(items, list.skip(lastIndex));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!next) {
|
||||||
|
mutable.set('hasMore', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
mutable.set('isLoading', false);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterNotifications = (state, relationship) => {
|
const filterNotifications = (state, relationship) => {
|
||||||
|
@ -97,27 +99,27 @@ const deleteByStatus = (state, statusId) => {
|
||||||
|
|
||||||
export default function notifications(state = initialState, action) {
|
export default function notifications(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case NOTIFICATIONS_REFRESH_REQUEST:
|
|
||||||
case NOTIFICATIONS_EXPAND_REQUEST:
|
case NOTIFICATIONS_EXPAND_REQUEST:
|
||||||
return state.set('isLoading', true);
|
return state.set('isLoading', true);
|
||||||
case NOTIFICATIONS_REFRESH_FAIL:
|
|
||||||
case NOTIFICATIONS_EXPAND_FAIL:
|
case NOTIFICATIONS_EXPAND_FAIL:
|
||||||
return state.set('isLoading', false);
|
return state.set('isLoading', false);
|
||||||
case NOTIFICATIONS_SCROLL_TOP:
|
case NOTIFICATIONS_SCROLL_TOP:
|
||||||
return updateTop(state, action.top);
|
return updateTop(state, action.top);
|
||||||
case NOTIFICATIONS_UPDATE:
|
case NOTIFICATIONS_UPDATE:
|
||||||
return normalizeNotification(state, action.notification);
|
return normalizeNotification(state, action.notification);
|
||||||
case NOTIFICATIONS_REFRESH_SUCCESS:
|
|
||||||
return normalizeNotifications(state, action.notifications, action.next);
|
|
||||||
case NOTIFICATIONS_EXPAND_SUCCESS:
|
case NOTIFICATIONS_EXPAND_SUCCESS:
|
||||||
return appendNormalizedNotifications(state, action.notifications, action.next);
|
return expandNormalizedNotifications(state, action.notifications, action.next);
|
||||||
case ACCOUNT_BLOCK_SUCCESS:
|
case ACCOUNT_BLOCK_SUCCESS:
|
||||||
case ACCOUNT_MUTE_SUCCESS:
|
case ACCOUNT_MUTE_SUCCESS:
|
||||||
return filterNotifications(state, action.relationship);
|
return filterNotifications(state, action.relationship);
|
||||||
case NOTIFICATIONS_CLEAR:
|
case NOTIFICATIONS_CLEAR:
|
||||||
return state.set('items', ImmutableList()).set('next', null);
|
return state.set('items', ImmutableList()).set('hasMore', false);
|
||||||
case TIMELINE_DELETE:
|
case TIMELINE_DELETE:
|
||||||
return deleteByStatus(state, action.id);
|
return deleteByStatus(state, action.id);
|
||||||
|
case TIMELINE_DISCONNECT:
|
||||||
|
return action.timeline === 'home' ?
|
||||||
|
state.update('items', items => items.first() ? items.unshift(null) : items) :
|
||||||
|
state;
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,87 +1,23 @@
|
||||||
import {
|
import {
|
||||||
REBLOG_REQUEST,
|
REBLOG_REQUEST,
|
||||||
REBLOG_SUCCESS,
|
|
||||||
REBLOG_FAIL,
|
REBLOG_FAIL,
|
||||||
UNREBLOG_SUCCESS,
|
|
||||||
FAVOURITE_REQUEST,
|
FAVOURITE_REQUEST,
|
||||||
FAVOURITE_SUCCESS,
|
|
||||||
FAVOURITE_FAIL,
|
FAVOURITE_FAIL,
|
||||||
UNFAVOURITE_SUCCESS,
|
|
||||||
PIN_SUCCESS,
|
|
||||||
UNPIN_SUCCESS,
|
|
||||||
} from '../actions/interactions';
|
} from '../actions/interactions';
|
||||||
import {
|
import {
|
||||||
STATUS_FETCH_SUCCESS,
|
|
||||||
CONTEXT_FETCH_SUCCESS,
|
|
||||||
STATUS_MUTE_SUCCESS,
|
STATUS_MUTE_SUCCESS,
|
||||||
STATUS_UNMUTE_SUCCESS,
|
STATUS_UNMUTE_SUCCESS,
|
||||||
STATUS_REVEAL,
|
STATUS_REVEAL,
|
||||||
STATUS_HIDE,
|
STATUS_HIDE,
|
||||||
} from '../actions/statuses';
|
} from '../actions/statuses';
|
||||||
import {
|
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||||
TIMELINE_REFRESH_SUCCESS,
|
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
|
||||||
TIMELINE_UPDATE,
|
|
||||||
TIMELINE_DELETE,
|
|
||||||
TIMELINE_EXPAND_SUCCESS,
|
|
||||||
} from '../actions/timelines';
|
|
||||||
import {
|
|
||||||
NOTIFICATIONS_UPDATE,
|
|
||||||
NOTIFICATIONS_REFRESH_SUCCESS,
|
|
||||||
NOTIFICATIONS_EXPAND_SUCCESS,
|
|
||||||
} from '../actions/notifications';
|
|
||||||
import {
|
|
||||||
FAVOURITED_STATUSES_FETCH_SUCCESS,
|
|
||||||
FAVOURITED_STATUSES_EXPAND_SUCCESS,
|
|
||||||
} from '../actions/favourites';
|
|
||||||
import {
|
|
||||||
PINNED_STATUSES_FETCH_SUCCESS,
|
|
||||||
} from '../actions/pin_statuses';
|
|
||||||
import { SEARCH_FETCH_SUCCESS } from '../actions/search';
|
|
||||||
import emojify from '../features/emoji/emoji';
|
|
||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||||
import escapeTextContentForBrowser from 'escape-html';
|
|
||||||
|
|
||||||
const domParser = new DOMParser();
|
const importStatus = (state, status) => state.set(status.id, fromJS(status));
|
||||||
|
|
||||||
const normalizeStatus = (state, status) => {
|
const importStatuses = (state, statuses) =>
|
||||||
if (!status) {
|
state.withMutations(mutable => statuses.forEach(status => importStatus(mutable, 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only calculate these values when status first encountered
|
|
||||||
// Otherwise keep the ones already in the reducer
|
|
||||||
if (!state.has(status.id)) {
|
|
||||||
const searchContent = [status.spoiler_text, status.content].join('\n\n').replace(/<br\s*\/?>/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);
|
|
||||||
normalStatus.hidden = normalStatus.sensitive;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) => {
|
const deleteStatus = (state, id, references) => {
|
||||||
references.forEach(ref => {
|
references.forEach(ref => {
|
||||||
|
@ -95,17 +31,10 @@ const initialState = ImmutableMap();
|
||||||
|
|
||||||
export default function statuses(state = initialState, action) {
|
export default function statuses(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case TIMELINE_UPDATE:
|
case STATUS_IMPORT:
|
||||||
case STATUS_FETCH_SUCCESS:
|
return importStatus(state, action.status);
|
||||||
case NOTIFICATIONS_UPDATE:
|
case STATUSES_IMPORT:
|
||||||
return normalizeStatus(state, action.status);
|
return importStatuses(state, action.statuses);
|
||||||
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:
|
case FAVOURITE_REQUEST:
|
||||||
return state.setIn([action.status.get('id'), 'favourited'], true);
|
return state.setIn([action.status.get('id'), 'favourited'], true);
|
||||||
case FAVOURITE_FAIL:
|
case FAVOURITE_FAIL:
|
||||||
|
@ -126,16 +55,6 @@ export default function statuses(state = initialState, action) {
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
action.ids.forEach(id => map.setIn([id, 'hidden'], true));
|
action.ids.forEach(id => map.setIn([id, 'hidden'], true));
|
||||||
});
|
});
|
||||||
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:
|
case TIMELINE_DELETE:
|
||||||
return deleteStatus(state, action.id, action.references);
|
return deleteStatus(state, action.id, action.references);
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
import {
|
import {
|
||||||
TIMELINE_REFRESH_REQUEST,
|
|
||||||
TIMELINE_REFRESH_SUCCESS,
|
|
||||||
TIMELINE_REFRESH_FAIL,
|
|
||||||
TIMELINE_UPDATE,
|
TIMELINE_UPDATE,
|
||||||
TIMELINE_DELETE,
|
TIMELINE_DELETE,
|
||||||
TIMELINE_EXPAND_SUCCESS,
|
TIMELINE_EXPAND_SUCCESS,
|
||||||
TIMELINE_EXPAND_REQUEST,
|
TIMELINE_EXPAND_REQUEST,
|
||||||
TIMELINE_EXPAND_FAIL,
|
TIMELINE_EXPAND_FAIL,
|
||||||
TIMELINE_SCROLL_TOP,
|
TIMELINE_SCROLL_TOP,
|
||||||
TIMELINE_CONNECT,
|
|
||||||
TIMELINE_DISCONNECT,
|
TIMELINE_DISCONNECT,
|
||||||
} from '../actions/timelines';
|
} from '../actions/timelines';
|
||||||
import {
|
import {
|
||||||
|
@ -22,37 +18,33 @@ const initialState = ImmutableMap();
|
||||||
|
|
||||||
const initialTimeline = ImmutableMap({
|
const initialTimeline = ImmutableMap({
|
||||||
unread: 0,
|
unread: 0,
|
||||||
online: false,
|
|
||||||
top: true,
|
top: true,
|
||||||
loaded: false,
|
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
next: false,
|
hasMore: true,
|
||||||
items: ImmutableList(),
|
items: ImmutableList(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizeTimeline = (state, timeline, statuses, next, isPartial) => {
|
const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial) => {
|
||||||
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) : oldIds.concat(ids));
|
|
||||||
mMap.set('isPartial', isPartial);
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
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 => {
|
return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
|
||||||
mMap.set('isLoading', false);
|
mMap.set('isLoading', false);
|
||||||
mMap.set('next', next);
|
if (!next) mMap.set('hasMore', false);
|
||||||
mMap.set('items', oldIds.concat(ids));
|
|
||||||
|
if (!statuses.isEmpty()) {
|
||||||
|
mMap.update('items', ImmutableList(), oldIds => {
|
||||||
|
const newIds = statuses.map(status => status.get('id'));
|
||||||
|
const lastIndex = oldIds.findLastIndex(id => id !== null && id >= newIds.last()) + 1;
|
||||||
|
const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && id > newIds.first());
|
||||||
|
|
||||||
|
if (firstIndex < 0) {
|
||||||
|
return (isPartial ? newIds.unshift(null) : newIds).concat(oldIds.skip(lastIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldIds.take(firstIndex + 1).concat(
|
||||||
|
isPartial && oldIds.get(firstIndex) !== null ? newIds.unshift(null) : newIds,
|
||||||
|
oldIds.skip(lastIndex)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -118,16 +110,12 @@ const updateTop = (state, timeline, top) => {
|
||||||
|
|
||||||
export default function timelines(state = initialState, action) {
|
export default function timelines(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case TIMELINE_REFRESH_REQUEST:
|
|
||||||
case TIMELINE_EXPAND_REQUEST:
|
case TIMELINE_EXPAND_REQUEST:
|
||||||
return state.update(action.timeline, initialTimeline, map => map.set('isLoading', true));
|
return state.update(action.timeline, initialTimeline, map => map.set('isLoading', true));
|
||||||
case TIMELINE_REFRESH_FAIL:
|
|
||||||
case TIMELINE_EXPAND_FAIL:
|
case TIMELINE_EXPAND_FAIL:
|
||||||
return state.update(action.timeline, initialTimeline, map => map.set('isLoading', false));
|
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, action.partial);
|
|
||||||
case TIMELINE_EXPAND_SUCCESS:
|
case TIMELINE_EXPAND_SUCCESS:
|
||||||
return appendNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next);
|
return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial);
|
||||||
case TIMELINE_UPDATE:
|
case TIMELINE_UPDATE:
|
||||||
return updateTimeline(state, action.timeline, fromJS(action.status));
|
return updateTimeline(state, action.timeline, fromJS(action.status));
|
||||||
case TIMELINE_DELETE:
|
case TIMELINE_DELETE:
|
||||||
|
@ -139,10 +127,15 @@ export default function timelines(state = initialState, action) {
|
||||||
return filterTimeline('home', state, action.relationship, action.statuses);
|
return filterTimeline('home', state, action.relationship, action.statuses);
|
||||||
case TIMELINE_SCROLL_TOP:
|
case TIMELINE_SCROLL_TOP:
|
||||||
return updateTop(state, action.timeline, action.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:
|
case TIMELINE_DISCONNECT:
|
||||||
return state.update(action.timeline, initialTimeline, map => map.set('online', false));
|
return state.update(
|
||||||
|
action.timeline,
|
||||||
|
initialTimeline,
|
||||||
|
map => map.update(
|
||||||
|
'items',
|
||||||
|
items => items.first() ? items : items.unshift(null)
|
||||||
|
)
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import WebSocketClient from 'websocket.js';
|
import WebSocketClient from 'websocket.js';
|
||||||
|
|
||||||
export function connectStream(path, pollingRefresh = null, callbacks = () => ({ onConnect() {}, onDisconnect() {}, onReceive() {} })) {
|
export function connectStream(path, pollingRefresh = null, callbacks = () => ({ onDisconnect() {}, onReceive() {} })) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']);
|
const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']);
|
||||||
const accessToken = getState().getIn(['meta', 'access_token']);
|
const accessToken = getState().getIn(['meta', 'access_token']);
|
||||||
const { onConnect, onDisconnect, onReceive } = callbacks(dispatch, getState);
|
const { onDisconnect, onReceive } = callbacks(dispatch, getState);
|
||||||
let polling = null;
|
let polling = null;
|
||||||
|
|
||||||
const setupPolling = () => {
|
const setupPolling = () => {
|
||||||
|
@ -25,7 +25,6 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({
|
||||||
if (pollingRefresh) {
|
if (pollingRefresh) {
|
||||||
clearPolling();
|
clearPolling();
|
||||||
}
|
}
|
||||||
onConnect();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
disconnected () {
|
disconnected () {
|
||||||
|
@ -44,7 +43,6 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({
|
||||||
clearPolling();
|
clearPolling();
|
||||||
pollingRefresh(dispatch);
|
pollingRefresh(dispatch);
|
||||||
}
|
}
|
||||||
onConnect();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -25,7 +25,6 @@ function main() {
|
||||||
const { getLocale } = require('../mastodon/locales');
|
const { getLocale } = require('../mastodon/locales');
|
||||||
const { localeData } = getLocale();
|
const { localeData } = getLocale();
|
||||||
const VideoContainer = require('../mastodon/containers/video_container').default;
|
const VideoContainer = require('../mastodon/containers/video_container').default;
|
||||||
const MediaGalleryContainer = require('../mastodon/containers/media_gallery_container').default;
|
|
||||||
const CardContainer = require('../mastodon/containers/card_container').default;
|
const CardContainer = require('../mastodon/containers/card_container').default;
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const ReactDOM = require('react-dom');
|
const ReactDOM = require('react-dom');
|
||||||
|
@ -76,15 +75,20 @@ function main() {
|
||||||
ReactDOM.render(<VideoContainer locale={locale} {...props} />, content);
|
ReactDOM.render(<VideoContainer locale={locale} {...props} />, content);
|
||||||
});
|
});
|
||||||
|
|
||||||
[].forEach.call(document.querySelectorAll('[data-component="MediaGallery"]'), (content) => {
|
|
||||||
const props = JSON.parse(content.getAttribute('data-props'));
|
|
||||||
ReactDOM.render(<MediaGalleryContainer locale={locale} {...props} />, content);
|
|
||||||
});
|
|
||||||
|
|
||||||
[].forEach.call(document.querySelectorAll('[data-component="Card"]'), (content) => {
|
[].forEach.call(document.querySelectorAll('[data-component="Card"]'), (content) => {
|
||||||
const props = JSON.parse(content.getAttribute('data-props'));
|
const props = JSON.parse(content.getAttribute('data-props'));
|
||||||
ReactDOM.render(<CardContainer locale={locale} {...props} />, content);
|
ReactDOM.render(<CardContainer locale={locale} {...props} />, content);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mediaGalleries = document.querySelectorAll('[data-component="MediaGallery"]');
|
||||||
|
|
||||||
|
if (mediaGalleries.length > 0) {
|
||||||
|
const MediaGalleriesContainer = require('../mastodon/containers/media_galleries_container').default;
|
||||||
|
const content = document.createElement('div');
|
||||||
|
|
||||||
|
ReactDOM.render(<MediaGalleriesContainer locale={locale} galleries={mediaGalleries} />, content);
|
||||||
|
document.body.appendChild(content);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
|
delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
|
||||||
|
|
|
@ -1435,14 +1435,19 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
&.image-loader--loading {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-content: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
.image-loader__preview-canvas {
|
.image-loader__preview-canvas {
|
||||||
filter: blur(2px);
|
max-width: $media-modal-media-max-width;
|
||||||
|
max-height: $media-modal-media-max-height;
|
||||||
|
background: url('../images/void.png') repeat;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.image-loader--loading .image-loader__preview-canvas {
|
||||||
|
filter: blur(2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.image-loader--amorphous .image-loader__preview-canvas {
|
&.image-loader--amorphous .image-loader__preview-canvas {
|
||||||
|
@ -1455,7 +1460,16 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-content: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: $media-modal-media-max-width;
|
||||||
|
max-height: $media-modal-media-max-height;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigation-bar {
|
.navigation-bar {
|
||||||
|
@ -3375,13 +3389,14 @@ a.status-card {
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-root {
|
.modal-root {
|
||||||
|
position: relative;
|
||||||
transition: opacity 0.3s linear;
|
transition: opacity 0.3s linear;
|
||||||
will-change: opacity;
|
will-change: opacity;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-root__overlay {
|
.modal-root__overlay {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
@ -3390,7 +3405,7 @@ a.status-card {
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-root__container {
|
.modal-root__container {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -3421,27 +3436,6 @@ a.status-card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
img,
|
|
||||||
canvas,
|
|
||||||
video {
|
|
||||||
max-width: 100%;
|
|
||||||
/*
|
|
||||||
put margins on top and bottom of image to avoid the screen coverd by
|
|
||||||
image.
|
|
||||||
*/
|
|
||||||
max-height: 80%;
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
img,
|
|
||||||
canvas {
|
|
||||||
display: block;
|
|
||||||
background: url('../images/void.png') repeat;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-modal__closer {
|
.media-modal__closer {
|
||||||
|
|
|
@ -60,6 +60,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.media-gallery-standalone__body {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.account-header {
|
.account-header {
|
||||||
width: 400px;
|
width: 400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
|
@ -30,3 +30,8 @@ $ui-highlight-color: $classic-highlight-color !default; // Vibrant
|
||||||
|
|
||||||
// Language codes that uses CJK fonts
|
// Language codes that uses CJK fonts
|
||||||
$cjk-langs: ja, ko, zh-CN, zh-HK, zh-TW;
|
$cjk-langs: ja, ko, zh-CN, zh-HK, zh-TW;
|
||||||
|
|
||||||
|
// Variables for components
|
||||||
|
$media-modal-media-max-width: 100%;
|
||||||
|
// put margins on top and bottom of image to avoid the screen covered by image.
|
||||||
|
$media-modal-media-max-height: 80%;
|
||||||
|
|
|
@ -5,6 +5,7 @@ module Mastodon
|
||||||
class NotPermittedError < Error; end
|
class NotPermittedError < Error; end
|
||||||
class ValidationError < Error; end
|
class ValidationError < Error; end
|
||||||
class HostValidationError < ValidationError; end
|
class HostValidationError < ValidationError; end
|
||||||
|
class LengthValidationError < ValidationError; end
|
||||||
class RaceConditionError < Error; end
|
class RaceConditionError < Error; end
|
||||||
|
|
||||||
class UnexpectedResponseError < Error
|
class UnexpectedResponseError < Error
|
||||||
|
|
|
@ -13,14 +13,13 @@ class ProviderDiscovery < OEmbed::ProviderDiscovery
|
||||||
def discover_provider(url, **options)
|
def discover_provider(url, **options)
|
||||||
format = options[:format]
|
format = options[:format]
|
||||||
|
|
||||||
if options[:html]
|
html = if options[:html]
|
||||||
html = Nokogiri::HTML(options[:html])
|
Nokogiri::HTML(options[:html])
|
||||||
else
|
else
|
||||||
res = Request.new(:get, url).perform
|
Request.new(:get, url).perform do |res|
|
||||||
|
|
||||||
raise OEmbed::NotFound, url if res.code != 200 || res.mime_type != 'text/html'
|
raise OEmbed::NotFound, url if res.code != 200 || res.mime_type != 'text/html'
|
||||||
|
Nokogiri::HTML(res.body_with_limit)
|
||||||
html = Nokogiri::HTML(res.to_s)
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if format.nil? || format == :json
|
if format.nil? || format == :json
|
||||||
|
|
|
@ -33,11 +33,19 @@ class Request
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
http_client.headers(headers).public_send(@verb, @url.to_s, @options)
|
begin
|
||||||
|
response = http_client.headers(headers).public_send(@verb, @url.to_s, @options)
|
||||||
rescue => e
|
rescue => e
|
||||||
raise e.class, "#{e.message} on #{@url}", e.backtrace[0]
|
raise e.class, "#{e.message} on #{@url}", e.backtrace[0]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
yield response.extend(ClientLimit)
|
||||||
|
ensure
|
||||||
|
http_client.close
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def headers
|
def headers
|
||||||
(@account ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET)
|
(@account ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET)
|
||||||
end
|
end
|
||||||
|
@ -88,7 +96,34 @@ class Request
|
||||||
end
|
end
|
||||||
|
|
||||||
def http_client
|
def http_client
|
||||||
HTTP.timeout(:per_operation, timeout).follow(max_hops: 2)
|
@http_client ||= HTTP.timeout(:per_operation, timeout).follow(max_hops: 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClientLimit
|
||||||
|
def body_with_limit(limit = 1.megabyte)
|
||||||
|
raise Mastodon::LengthValidationError if content_length.present? && content_length > limit
|
||||||
|
|
||||||
|
if charset.nil?
|
||||||
|
encoding = Encoding::BINARY
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
encoding = Encoding.find(charset)
|
||||||
|
rescue ArgumentError
|
||||||
|
encoding = Encoding::BINARY
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
contents = String.new(encoding: encoding)
|
||||||
|
|
||||||
|
while (chunk = readpartial)
|
||||||
|
contents << chunk
|
||||||
|
chunk.clear
|
||||||
|
|
||||||
|
raise Mastodon::LengthValidationError if contents.bytesize > limit
|
||||||
|
end
|
||||||
|
|
||||||
|
contents
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class Socket < TCPSocket
|
class Socket < TCPSocket
|
||||||
|
@ -110,5 +145,5 @@ class Request
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private_constant :Socket
|
private_constant :ClientLimit, :Socket
|
||||||
end
|
end
|
||||||
|
|
|
@ -55,7 +55,6 @@ class Account < ApplicationRecord
|
||||||
include AccountHeader
|
include AccountHeader
|
||||||
include AccountInteractions
|
include AccountInteractions
|
||||||
include Attachmentable
|
include Attachmentable
|
||||||
include Remotable
|
|
||||||
include Paginable
|
include Paginable
|
||||||
|
|
||||||
enum protocol: [:ostatus, :activitypub]
|
enum protocol: [:ostatus, :activitypub]
|
||||||
|
|
|
@ -2,4 +2,5 @@
|
||||||
|
|
||||||
class ApplicationRecord < ActiveRecord::Base
|
class ApplicationRecord < ActiveRecord::Base
|
||||||
self.abstract_class = true
|
self.abstract_class = true
|
||||||
|
include Remotable
|
||||||
end
|
end
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue