Merge remote-tracking branch 'origin/develop' into snackbar

config-ui-updates
Alex Gleason 4 years ago
commit 81c31f5d92
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7

@ -1,7 +1,8 @@
import loadPolyfills from './soapbox/load_polyfills';
import { start } from './soapbox/common';
start();
require('fork-awesome/css/fork-awesome.css');
require.context('./images/', true);
loadPolyfills().then(() => {
require('./soapbox/main').default();

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 69 KiB

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="63.161953mm"
height="181.12712mm"
viewBox="0 0 63.161953 181.12712"
version="1.1"
id="svg1199"
inkscape:version="0.92.4 (unknown)"
sodipodi:docname="spider.svg">
<defs
id="defs1193" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35355339"
inkscape:cx="188.63933"
inkscape:cy="154.00309"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="705"
inkscape:window-x="0"
inkscape:window-y="30"
inkscape:window-maximized="1"
inkscape:snap-global="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<metadata
id="metadata1196">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-54.223528,-39.965002)">
<path
style="stroke-width:0.99999994"
d="m 329.96094,151.04883 -7.95132,372.20898 c -30.02705,2.9243 -45.57271,12.92382 -64.25977,32.67188 -25.16762,33.38088 -18.43249,69.4298 -0.4707,100.66992 12.24879,17.03193 32.3984,27.97627 53.07033,34.15036 0,0 -5.52814,0.0857 -11.58984,9.46094 -18.91001,-5.43999 -38.07073,-9.95039 -57.14063,-14.82032 -10.49976,0.9523 -28.58163,18.8274 -36.67969,24.9004 0.27746,13.19067 1.67361,27.14135 3.33008,39.15039 1.1699,-1.57002 0.83916,-3.5804 1.03906,-5.40039 0.9,-10.79003 0.60034,-21.66016 1.99024,-32.41016 9.28,-6.03999 17.7906,-13.20072 26.7207,-19.7207 18.99884,1.97067 39.37112,9.36858 55.91016,13.35156 -0.49,2.41999 -1.38047,5.27974 -4.23047,5.67969 -18.4,4.60002 -36.81969,9.10007 -55.17969,13.83007 -4.86555,6.81697 -23.47884,41.76065 -22.16992,48 3.32807,9.25919 3.76668,29.35751 8.58984,35.70899 -0.65616,-11.27353 -1.26587,-23.12102 -2.88086,-33.41016 4.366,-15.53732 14.77165,-31.85507 21.21094,-44.72851 16.36837,-5.03014 33.6873,-8.93673 49.58008,-11.32032 -0.0299,3.31998 -0.081,6.66013 -0.20117,9.99024 -10.89612,8.5036 -30.45632,23.65603 -40.40821,30.44922 -2.57681,15.80044 -3.38605,33.75066 -4.2207,48.55078 2.50279,8.85582 13.19431,23.74406 18.17156,23.90823 -2.93816,-7.30216 -8.51629,-14.68425 -10.88086,-21.31836 -0.17087,-16.87764 2.99403,-32.98356 3.70114,-48.41015 11.61344,-9.80937 25.4679,-15.10577 35.89062,-24.25 2.26541,6.18864 7.32913,9.97253 10.32813,15.05859 -2.15,3.10001 -5.51922,5.79 -5.94922,9.75 2.88,4.37998 6.60955,8.25101 10.68945,11.54102 -0.85,-3.43 -2.26023,-6.68056 -3.24023,-10.06055 l 6.20117,-7.18945 c 10.18753,5.69922 19.39911,4.81707 28.78906,0.75976 2.12,2.45 4.30149,5.11952 5.27149,8.26953 -0.85,3.26 -2.7418,6.14966 -3.5918,9.42969 4.21,-3.40003 8.09071,-7.32883 11.4707,-11.54883 -0.72,-4.08 -4.4693,-6.80104 -5.27929,-10.79101 3.66,-4.43003 7.97023,-8.42941 10.24023,-13.85938 5.68622,5.4072 34.43902,22.24881 34.94922,26.88086 0.36518,16.19209 3.11897,31.74502 2,46.75 -4.46916,8.68536 -7.12999,16.57554 -14.39063,22.67969 9.90723,0.50906 17.4253,-14.74937 21.52152,-22.69328 -0.18697,-17.91233 -0.74645,-33.39521 -1.16992,-49.66992 -13.47001,-10.57002 -27.16094,-20.89017 -40.46094,-31.66016 0.59,-3.81003 0.49976,-7.6583 0.50977,-11.48828 15.73,4.66001 31.80992,8.14868 47.66992,12.38867 7.58475,10.99663 15.5151,31.43552 20.24023,42.75977 0.43698,13.66208 -3.68079,27.5449 -4.08008,40.14062 1.49998,-1.33999 1.6498,-3.42013 2.17969,-5.24023 1.88197,-11.16719 9.61842,-29.63645 8.13086,-37.92969 -6.21997,-14.23003 -11.95978,-28.75009 -18.42969,-42.83008 -19.30273,-6.68031 -40.27482,-12.85569 -58.39062,-17.73047 -0.65,-1.72002 -1.1801,-3.47951 -1.5,-5.26953 17.78,-3.66999 35.60009,-7.40034 53.33008,-11.32031 5.35892,-0.14205 29.14876,22.09172 28.98047,23.98047 1.30016,6.78634 -2.08415,29.71011 1.61914,33.13086 2.05988,-11.02999 3.41097,-22.17002 5.12109,-33.25 -0.32862,-6.33401 -29.16337,-28.29439 -33.91016,-30.79102 -20.42635,4.13166 -40.67884,9.74123 -59.80078,12.63086 -5.16629,-4.96887 -11.64306,-7.41991 -17.4707,-10.33984 26.33,-1.87998 52.09,-16.02008 66.25,-38.58008 9.5235,-13.96814 12.87637,-29.769 13.1992,-45.79102 0.33714,-20.46694 -8.12112,-40.39069 -21.6211,-55.4707 -18.78284,-17.43524 -31.48782,-23.12017 -55.43945,-26.73828 l 6.93151,-372.80078 z"
id="path1768"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
transform="scale(0.26458333)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="svg46269" viewBox="0 0 340.00001 394.2857" height="111.27618mm" width="95.955559mm">
<defs id="defs46271">
<linearGradient id="linearGradient46839">
<stop id="stop46841" offset="0" style="stop-color:#904700;stop-opacity:1;"/>
<stop id="stop46843" offset="1" style="stop-color:#904700;stop-opacity:0;"/>
</linearGradient>
<linearGradient id="linearGradient46831">
<stop id="stop46833" offset="0" style="stop-color:#904700;stop-opacity:1;"/>
<stop id="stop46835" offset="1" style="stop-color:#904700;stop-opacity:0;"/>
</linearGradient>
<linearGradient id="linearGradient46823">
<stop id="stop46825" offset="0" style="stop-color:#904700;stop-opacity:1;"/>
<stop id="stop46827" offset="1" style="stop-color:#904700;stop-opacity:0;"/>
</linearGradient>
<radialGradient gradientTransform="matrix(4.9019612,0,0,4.9019612,-600.72836,-1264.1473)" gradientUnits="userSpaceOnUse" r="72.85714" fy="330.93362" fx="152.85715" cy="330.93362" cx="152.85715" id="radialGradient46829" xlink:href="#linearGradient46823"/>
<radialGradient gradientTransform="matrix(3.3636365,0,0,3.3636365,-602.85717,-938.05096)" gradientUnits="userSpaceOnUse" r="62.857143" fy="429.50507" fx="251.42857" cy="429.50507" cx="251.42857" id="radialGradient46837" xlink:href="#linearGradient46831"/>
<radialGradient gradientTransform="matrix(1.7317072,0,0,1.7317072,-145.78397,-287.44272)" gradientUnits="userSpaceOnUse" r="58.57143" fy="470.93369" fx="132.85715" cy="470.93369" cx="132.85715" id="radialGradient46845" xlink:href="#linearGradient46839"/>
</defs>
<metadata id="metadata46274">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g transform="translate(-8.5714264,-218.07648)" id="layer1">
<circle r="140" cy="358.07648" cx="148.57143" id="path46817" style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:url(#radialGradient46829);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:20, 5;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"/>
<circle r="105.71429" cy="506.64789" cx="242.85715" id="path46819" style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:url(#radialGradient46837);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:20, 5;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"/>
<circle r="58.57143" cy="528.07654" cx="84.285713" id="path46821" style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:url(#radialGradient46845);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:20, 5;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

@ -145,7 +145,17 @@ export function logIn(username, password) {
export function logOut() {
return (dispatch, getState) => {
const state = getState();
dispatch({ type: AUTH_LOGGED_OUT });
// Attempt to destroy OAuth token on logout
api(getState).post('/oauth/revoke', {
client_id: state.getIn(['auth', 'app', 'client_id']),
client_secret: state.getIn(['auth', 'app', 'client_secret']),
token: state.getIn(['auth', 'user', 'access_token']),
});
dispatch(showAlert('Successfully logged out.', ''));
};
}

@ -23,6 +23,10 @@ export const CHAT_READ_REQUEST = 'CHAT_READ_REQUEST';
export const CHAT_READ_SUCCESS = 'CHAT_READ_SUCCESS';
export const CHAT_READ_FAIL = 'CHAT_READ_FAIL';
export const CHAT_MESSAGE_DELETE_REQUEST = 'CHAT_MESSAGE_DELETE_REQUEST';
export const CHAT_MESSAGE_DELETE_SUCCESS = 'CHAT_MESSAGE_DELETE_SUCCESS';
export const CHAT_MESSAGE_DELETE_FAIL = 'CHAT_MESSAGE_DELETE_FAIL';
export function fetchChats() {
return (dispatch, getState) => {
dispatch({ type: CHATS_FETCH_REQUEST });
@ -150,3 +154,14 @@ export function markChatRead(chatId, lastReadId) {
});
};
}
export function deleteChatMessage(chatId, messageId) {
return (dispatch, getState) => {
dispatch({ type: CHAT_MESSAGE_DELETE_REQUEST, chatId, messageId });
api(getState).delete(`/api/v1/pleroma/chats/${chatId}/messages/${messageId}`).then(({ data }) => {
dispatch({ type: CHAT_MESSAGE_DELETE_SUCCESS, chatId, messageId, chatMessage: data });
}).catch(error => {
dispatch({ type: CHAT_MESSAGE_DELETE_FAIL, chatId, messageId, error });
});
};
}

@ -143,13 +143,13 @@ export function handleComposeSubmit(dispatch, getState, response, status) {
let dequeueArgs = {};
if (timelineId === 'community') dequeueArgs.onlyMedia = getSettings(getState()).getIn(['community', 'other', 'onlyMedia']);
dispatch(dequeueTimeline(timelineId, null, dequeueArgs));
dispatch(updateTimeline(timelineId, { ...response.data }));
dispatch(updateTimeline(timelineId, response.data.id));
}
};
if (response.data.visibility !== 'direct') {
insertIfOnline('home');
} else if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
} else if (response.data.visibility === 'public') {
insertIfOnline('community');
insertIfOnline('public');
}
@ -440,17 +440,6 @@ export function updateTagHistory(tags) {
};
}
export function hydrateCompose() {
return (dispatch, getState) => {
const me = getState().get('me');
const history = tagHistory.get(me);
if (history !== null) {
dispatch(updateTagHistory(history));
}
};
}
function insertIntoTagHistory(recognizedTags, text) {
return (dispatch, getState) => {
const state = getState();

@ -0,0 +1,56 @@
import api from '../api';
import { showAlert } from 'soapbox/actions/alerts';
export const IMPORT_FOLLOWS_REQUEST = 'IMPORT_FOLLOWS_REQUEST';
export const IMPORT_FOLLOWS_SUCCESS = 'IMPORT_FOLLOWS_SUCCESS';
export const IMPORT_FOLLOWS_FAIL = 'IMPORT_FOLLOWS_FAIL';
export const IMPORT_BLOCKS_REQUEST = 'IMPORT_BLOCKS_REQUEST';
export const IMPORT_BLOCKS_SUCCESS = 'IMPORT_BLOCKS_SUCCESS';
export const IMPORT_BLOCKS_FAIL = 'IMPORT_BLOCKS_FAIL';
export const IMPORT_MUTES_REQUEST = 'IMPORT_MUTES_REQUEST';
export const IMPORT_MUTES_SUCCESS = 'IMPORT_MUTES_SUCCESS';
export const IMPORT_MUTES_FAIL = 'IMPORT_MUTES_FAIL';
export function importFollows(params) {
return (dispatch, getState) => {
dispatch({ type: IMPORT_FOLLOWS_REQUEST });
return api(getState)
.post('/api/pleroma/follow_import', params)
.then(response => {
dispatch(showAlert('', 'Followers imported successfully'));
dispatch({ type: IMPORT_FOLLOWS_SUCCESS, config: response.data });
}).catch(error => {
dispatch({ type: IMPORT_FOLLOWS_FAIL, error });
});
};
}
export function importBlocks(params) {
return (dispatch, getState) => {
dispatch({ type: IMPORT_BLOCKS_REQUEST });
return api(getState)
.post('/api/pleroma/blocks_import', params)
.then(response => {
dispatch(showAlert('', 'Blocks imported successfully'));
dispatch({ type: IMPORT_BLOCKS_SUCCESS, config: response.data });
}).catch(error => {
dispatch({ type: IMPORT_BLOCKS_FAIL, error });
});
};
}
export function importMutes(params) {
return (dispatch, getState) => {
dispatch({ type: IMPORT_MUTES_REQUEST });
return api(getState)
.post('/api/pleroma/mutes_import', params)
.then(response => {
dispatch(showAlert('', 'Mutes imported successfully'));
dispatch({ type: IMPORT_MUTES_SUCCESS, config: response.data });
}).catch(error => {
dispatch({ type: IMPORT_MUTES_FAIL, error });
});
};
}

@ -10,7 +10,11 @@ import {
} from './importer';
import { getSettings, saveSettings } from './settings';
import { defineMessages } from 'react-intl';
import { List as ImmutableList } from 'immutable';
import {
List as ImmutableList,
Map as ImmutableMap,
OrderedMap as ImmutableOrderedMap,
} from 'immutable';
import { unescapeHTML } from '../utils/html';
import { getFilters, regexFromFilters } from '../selectors';
@ -121,7 +125,7 @@ export function updateNotificationsQueue(notification, intlMessages, intlLocale,
export function dequeueNotifications() {
return (dispatch, getState) => {
const queuedNotifications = getState().getIn(['notifications', 'queuedNotifications'], ImmutableList());
const queuedNotifications = getState().getIn(['notifications', 'queuedNotifications'], ImmutableOrderedMap());
const totalQueuedNotificationsCount = getState().getIn(['notifications', 'totalQueuedNotificationsCount'], 0);
if (totalQueuedNotificationsCount === 0) {
@ -252,9 +256,12 @@ export function setFilter(filterType) {
export function markReadNotifications() {
return (dispatch, getState) => {
if (!getState().get('me')) return;
const topNotification = parseInt(getState().getIn(['notifications', 'items', 0, 'id']));
const lastRead = getState().getIn(['notifications', 'lastRead']);
const state = getState();
if (!state.get('me')) return;
const topNotification = state.getIn(['notifications', 'items'], ImmutableOrderedMap()).first(ImmutableMap()).get('id');
const lastRead = state.getIn(['notifications', 'lastRead']);
if (!(topNotification && topNotification > lastRead)) return;
dispatch({

@ -25,6 +25,17 @@ export function initReport(account, status) {
};
};
export function initReportById(accountId) {
return (dispatch, getState) => {
dispatch({
type: REPORT_INIT,
account: getState().getIn(['accounts', accountId]),
});
dispatch(openModal('REPORT'));
};
};
export function cancelReport() {
return {
type: REPORT_CANCEL,

@ -2,6 +2,7 @@ import { debounce } from 'lodash';
import { showAlertForError } from './alerts';
import { patchMe } from 'soapbox/actions/me';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import uuid from '../uuid';
export const SETTING_CHANGE = 'SETTING_CHANGE';
export const SETTING_SAVE = 'SETTING_SAVE';
@ -114,6 +115,12 @@ const defaultSettings = ImmutableMap({
trends: ImmutableMap({
show: true,
}),
columns: ImmutableList([
ImmutableMap({ id: 'COMPOSE', uuid: uuid(), params: {} }),
ImmutableMap({ id: 'HOME', uuid: uuid(), params: {} }),
ImmutableMap({ id: 'NOTIFICATIONS', uuid: uuid(), params: {} }),
]),
});
export function getSettings(state) {

@ -1,23 +0,0 @@
import { Iterable, fromJS } from 'immutable';
import { hydrateCompose } from './compose';
export const STORE_HYDRATE = 'STORE_HYDRATE';
export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
const convertState = rawState =>
fromJS(rawState, (k, v) =>
Iterable.isIndexed(v) ? v.toList() : v.toMap());
export function hydrateStore(rawState) {
return dispatch => {
const state = convertState(rawState);
dispatch({
type: STORE_HYDRATE,
state,
});
dispatch(hydrateCompose());
// dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
};
};

@ -25,31 +25,31 @@ export function processTimelineUpdate(timeline, status, accept) {
const columnSettings = getSettings(getState()).get(timeline, ImmutableMap());
const shouldSkipQueue = shouldFilter(fromJS(status), columnSettings);
dispatch(importFetchedStatus(status));
if (shouldSkipQueue) {
return dispatch(updateTimeline(timeline, status, accept));
return dispatch(updateTimeline(timeline, status.id, accept));
} else {
return dispatch(updateTimelineQueue(timeline, status, accept));
return dispatch(updateTimelineQueue(timeline, status.id, accept));
}
};
}
export function updateTimeline(timeline, status, accept) {
export function updateTimeline(timeline, statusId, accept) {
return dispatch => {
if (typeof accept === 'function' && !accept(status)) {
return;
}
dispatch(importFetchedStatus(status));
dispatch({
type: TIMELINE_UPDATE,
timeline,
status,
statusId,
});
};
};
export function updateTimelineQueue(timeline, status, accept) {
export function updateTimelineQueue(timeline, statusId, accept) {
return dispatch => {
if (typeof accept === 'function' && !accept(status)) {
return;
@ -58,7 +58,7 @@ export function updateTimelineQueue(timeline, status, accept) {
dispatch({
type: TIMELINE_UPDATE_QUEUE,
timeline,
status,
statusId,
});
};
};
@ -73,8 +73,8 @@ export function dequeueTimeline(timeline, expandFunc, optionalExpandArgs) {
if (totalQueuedItemsCount === 0) {
return;
} else if (totalQueuedItemsCount > 0 && totalQueuedItemsCount <= MAX_QUEUED_ITEMS) {
queuedItems.forEach(status => {
dispatch(updateTimeline(timeline, status.toJS(), null));
queuedItems.forEach(statusId => {
dispatch(updateTimeline(timeline, statusId, null));
});
} else {
if (typeof expandFunc === 'function') {

@ -1,14 +0,0 @@
'use strict';
import Rails from 'rails-ujs';
export function start() {
require('fork-awesome/css/fork-awesome.css');
require.context('../images/', true);
try {
Rails.start();
} catch (e) {
// If called twice
}
};

@ -50,6 +50,8 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
autoFocus: PropTypes.bool,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
clickableAreaRef: PropTypes.object,
getClickableArea: PropTypes.func.isRequired,
};
static defaultProps = {
@ -107,6 +109,8 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
} else {
this.setState({ lastToken: null });
}
break;
@ -114,6 +118,8 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
} else {
this.setState({ lastToken: null });
}
break;
@ -159,6 +165,27 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
this.textarea.focus();
}
isClickInside = (e) => {
return [
this.props.getClickableArea(),
document.querySelector('.autosuggest-textarea__textarea'),
].some(element => element && element.contains(e.target));
}
handleClick = (e) => {
if (this.isClickInside(e)) {
this.setState({ lastToken: null });
}
}
componentDidMount() {
document.addEventListener('click', this.handleClick, true);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleClick, true);
}
componentDidUpdate(prevProps, prevState) {
const { suggestions } = this.props;
if (suggestions !== prevProps.suggestions && suggestions.size > 0 && prevState.suggestionsHidden && prevState.focused) {

@ -1,5 +1,5 @@
import React from 'react';
import { Sparklines, SparklinesCurve } from 'react-sparklines';
// import { Sparklines, SparklinesCurve } from 'react-sparklines';
import { FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Permalink from './permalink';
@ -17,11 +17,12 @@ const Hashtag = ({ hashtag }) => (
</div>}
</div>
{hashtag.get('history') && <div className='trends__item__sparkline'>
{/* Pleroma doesn't support tag history yet */}
{/* hashtag.get('history') && <div className='trends__item__sparkline'>
<Sparklines width={50} height={28} data={hashtag.get('history').reverse().map(day => day.get('uses')).toArray()}>
<SparklinesCurve style={{ fill: 'none' }} />
</Sparklines>
</div>}
</div> */}
</div>
);

@ -29,6 +29,7 @@ const messages = defineMessages({
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
admin_settings: { id: 'navigation_bar.admin_settings', defaultMessage: 'Admin settings' },
soapbox_config: { id: 'navigation_bar.soapbox_config', defaultMessage: 'Soapbox config' },
import_data: { id: 'navigation_bar.import_data', defaultMessage: 'Import data' },
security: { id: 'navigation_bar.security', defaultMessage: 'Security' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
lists: { id: 'column.lists', defaultMessage: 'Lists' },
@ -180,6 +181,10 @@ class SidebarMenu extends ImmutablePureComponent {
<Icon id='cog' />
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.preferences)}</span>
</NavLink>
<NavLink className='sidebar-menu-item' to='/settings/import' onClick={onClose}>
<Icon id='cloud-upload' />
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.import_data)}</span>
</NavLink>
<NavLink className='sidebar-menu-item' to='/auth/edit' onClick={onClose}>
<Icon id='lock' />
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.security)}</span>

@ -14,8 +14,6 @@ import { ScrollContext } from 'react-router-scroll-4';
import UI from '../features/ui';
// import Introduction from '../features/introduction';
import { fetchCustomEmojis } from '../actions/custom_emojis';
import { hydrateStore } from '../actions/store';
import initialState from '../initial_state';
import { preload } from '../actions/preload';
import { IntlProvider } from 'react-intl';
import ErrorBoundary from '../components/error_boundary';
@ -32,9 +30,6 @@ const validLocale = locale => Object.keys(messages).includes(locale);
export const store = configureStore();
const hydrateAction = hydrateStore(initialState);
store.dispatch(hydrateAction);
store.dispatch(preload());
store.dispatch(fetchMe());
store.dispatch(fetchInstance());
@ -59,6 +54,7 @@ const mapStateToProps = (state) => {
locale: validLocale(locale) ? locale : 'en',
themeCss: generateThemeCss(soapboxConfig.get('brandColor')),
themeMode: settings.get('themeMode'),
halloween: settings.get('halloween'),
customCss: soapboxConfig.get('customCss'),
};
};
@ -77,6 +73,7 @@ class SoapboxMount extends React.PureComponent {
themeCss: PropTypes.string,
themeMode: PropTypes.string,
customCss: ImmutablePropTypes.list,
halloween: PropTypes.bool,
dispatch: PropTypes.func,
};
@ -122,6 +119,7 @@ class SoapboxMount extends React.PureComponent {
'no-reduce-motion': !this.props.reduceMotion,
'dyslexic': this.props.dyslexicFont,
'demetricator': this.props.demetricator,
'halloween': this.props.halloween,
});
return (

@ -20,17 +20,18 @@ class LoginPage extends ImmutablePureComponent {
this.handleSubmit = this.handleSubmit.bind(this);
}
state = {
isLoading: false,
mfa_auth_needed: false,
mfa_token: '',
}
getFormData = (form) => {
return Object.fromEntries(
Array.from(form).map(i => [i.name, i.value])
);
}
state = {
mfa_auth_needed: false,
mfa_token: '',
}
handleSubmit = (event) => {
const { dispatch } = this.props;
const { username, password } = this.getFormData(event.target);
@ -47,8 +48,8 @@ class LoginPage extends ImmutablePureComponent {
}
render() {
const { me, isLoading } = this.props;
const { mfa_auth_needed, mfa_token } = this.state;
const { me } = this.props;
const { isLoading, mfa_auth_needed, mfa_token } = this.state;
if (me) return <Redirect to='/' />;
if (mfa_auth_needed) return <OtpAuthForm mfa_token={mfa_token} />;

@ -18,6 +18,7 @@ import IconButton from 'soapbox/components/icon_button';
const messages = defineMessages({
placeholder: { id: 'chat_box.input.placeholder', defaultMessage: 'Send a message…' },
send: { id: 'chat_box.actions.send', defaultMessage: 'Send' },
});
const mapStateToProps = (state, { chatId }) => ({
@ -164,11 +165,17 @@ class ChatBox extends ImmutablePureComponent {
}
renderActionButton = () => {
const { intl } = this.props;
const { resetFileKey } = this.state;
return this.canSubmit() ? (
<div className='chat-box__send'>
<IconButton icon='send' size={16} onClick={this.sendMessage} />
<IconButton
icon='send'
title={intl.formatMessage(messages.send)}
size={16}
onClick={this.sendMessage}
/>
</div>
) : (
<UploadButton onSelectFile={this.handleFiles} resetFileKey={resetFileKey} />

@ -5,16 +5,21 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, defineMessages } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { fetchChatMessages } from 'soapbox/actions/chats';
import { fetchChatMessages, deleteChatMessage } from 'soapbox/actions/chats';
import emojify from 'soapbox/features/emoji/emoji';
import classNames from 'classnames';
import { openModal } from 'soapbox/actions/modal';
import { escape, throttle } from 'lodash';
import { MediaGallery } from 'soapbox/features/ui/util/async-components';
import Bundle from 'soapbox/features/ui/components/bundle';
import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container';
import { initReportById } from 'soapbox/actions/reports';
const messages = defineMessages({
today: { id: 'chats.dividers.today', defaultMessage: 'Today' },
more: { id: 'chats.actions.more', defaultMessage: 'More' },
delete: { id: 'chats.actions.delete', defaultMessage: 'Delete message' },
report: { id: 'chats.actions.report', defaultMessage: 'Report user' },
});
const timeChange = (prev, curr) => {
@ -198,7 +203,8 @@ class ChatMessageList extends ImmutablePureComponent {
parseContent = chatMessage => {
const content = chatMessage.get('content') || '';
const pending = chatMessage.get('pending', false);
const formatted = pending ? this.parsePendingContent(content) : content;
const deleting = chatMessage.get('deleting', false);
const formatted = (pending && !deleting) ? this.parsePendingContent(content) : content;
const emojiMap = makeEmojiMap(chatMessage);
return emojify(formatted, emojiMap.toJS());
}
@ -207,12 +213,28 @@ class ChatMessageList extends ImmutablePureComponent {
this.node = c;
}
renderDivider = (text) => (
<div className='chat-messages__divider'>{text}</div>
renderDivider = (key, text) => (
<div className='chat-messages__divider' key={key}>{text}</div>
)
handleDeleteMessage = (chatId, messageId) => {
return () => {
this.props.dispatch(deleteChatMessage(chatId, messageId));
};
}
handleReportUser = (userId) => {
return () => {
this.props.dispatch(initReportById(userId));
};
}
renderMessage = (chatMessage) => {
const { me } = this.props;
const { me, intl } = this.props;
const menu = [
{ text: intl.formatMessage(messages.delete), action: this.handleDeleteMessage(chatMessage.get('chat_id'), chatMessage.get('id')) },
{ text: intl.formatMessage(messages.report), action: this.handleReportUser(chatMessage.get('account_id')) },
];
return (
<div
@ -226,12 +248,22 @@ class ChatMessageList extends ImmutablePureComponent {
title={this.getFormattedTimestamp(chatMessage)}
className='chat-message__bubble'
ref={this.setBubbleRef}
tabIndex={0}
>
{this.maybeRenderMedia(chatMessage)}
<span
className='chat-message__content'
dangerouslySetInnerHTML={{ __html: this.parseContent(chatMessage) }}
/>
<div className='chat-message__menu'>
<DropdownMenuContainer
items={menu}
icon='ellipsis-h'
size={18}
direction='top'
title={intl.formatMessage(messages.more)}
/>
</div>
</div>
</div>
);
@ -246,12 +278,13 @@ class ChatMessageList extends ImmutablePureComponent {
const lastMessage = chatMessages.get(idx-1);
if (lastMessage) {
const key = `${curr.get('id')}_divider`;
switch(timeChange(lastMessage, curr)) {
case 'today':
acc.push(this.renderDivider(intl.formatMessage(messages.today)));
acc.push(this.renderDivider(key, intl.formatMessage(messages.today)));
break;
case 'date':
acc.push(this.renderDivider(new Date(curr.get('created_at')).toDateString()));
acc.push(this.renderDivider(key, new Date(curr.get('created_at')).toDateString()));
break;
}
}

@ -20,6 +20,7 @@ const messages = defineMessages({
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
admin_settings: { id: 'navigation_bar.admin_settings', defaultMessage: 'Admin settings' },
soapbox_config: { id: 'navigation_bar.soapbox_config', defaultMessage: 'Soapbox config' },
import_data: { id: 'navigation_bar.import_data', defaultMessage: 'Import data' },
security: { id: 'navigation_bar.security', defaultMessage: 'Security' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Hotkeys' },
@ -84,6 +85,7 @@ class ActionBar extends React.PureComponent {
menu.push({ text: intl.formatMessage(messages.soapbox_config), to: '/soapbox/config' });
}
menu.push({ text: intl.formatMessage(messages.preferences), to: '/settings/preferences' });
menu.push({ text: intl.formatMessage(messages.import_data), to: '/settings/import' });
menu.push({ text: intl.formatMessage(messages.security), to: '/auth/edit' });
menu.push({ text: intl.formatMessage(messages.logout), to: '/auth/sign_out', action: onClickLogOut });

@ -286,6 +286,7 @@ class ComposeForm extends ImmutablePureComponent {
onSuggestionSelected={this.onSuggestionSelected}
onPaste={onPaste}
autoFocus={shouldAutoFocus}
getClickableArea={this.getClickableArea}
>
{
!condensed &&

@ -17,7 +17,10 @@ const mapDispatchToProps = dispatch => ({
isUserTouching,
onModalOpen: props => dispatch(openModal('ACTIONS', props)),
onModalClose: () => dispatch(closeModal()),
onModalClose: () => {
dispatch(closeModal());
dispatch(openModal('COMPOSE'));
},
});

@ -55,6 +55,9 @@ exports[`<RadioGroup /> renders correctly 1`] = `
`;
exports[`<SelectDropdown /> renders correctly 1`] = `
<div
class="select-wrapper"
>
<select>
<option
value="one"
@ -72,6 +75,7 @@ exports[`<SelectDropdown /> renders correctly 1`] = `
Three
</option>
</select>
</div>
`;
exports[`<SimpleForm /> renders correctly 1`] = `

@ -3,12 +3,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { v4 as uuidv4 } from 'uuid';
import { SketchPicker } from 'react-color';
import Overlay from 'react-overlays/lib/Overlay';
import { isMobile } from '../../is_mobile';
import detectPassiveEvents from 'detect-passive-events';
const FormPropTypes = {
export const FormPropTypes = {
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
@ -16,8 +12,6 @@ const FormPropTypes = {
]),
};
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
export const InputContainer = (props) => {
const containerClass = classNames('input', {
'with_label': props.label,
@ -192,98 +186,6 @@ export class RadioGroup extends ImmutablePureComponent {
}
export class ColorPicker extends React.PureComponent {
static propTypes = {
style: PropTypes.object,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
onClose: PropTypes.func,
}
handleDocumentClick = e => {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose();
}
}
componentDidMount() {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleDocumentClick, false);
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
setRef = c => {
this.node = c;
}
render() {
const { style, value, onChange } = this.props;
let margin_left_picker = isMobile(window.innerWidth) ? '20px' : '12px';
return (
<div id='SketchPickerContainer' ref={this.setRef} style={{ ...style, marginLeft: margin_left_picker, position: 'absolute', zIndex: 1000 }}>
<SketchPicker color={value} disableAlpha onChange={onChange} />
</div>
);
}
}
export class ColorWithPicker extends ImmutablePureComponent {
static propTypes = {
buttonId: PropTypes.string.isRequired,
label: FormPropTypes.label,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
}
onToggle = (e) => {
if (!e.key || e.key === 'Enter') {
if (this.state.active) {
this.onHidePicker();
} else {
this.onShowPicker(e);
}
}
}
state = {
active: false,
placement: null,
}
onHidePicker = () => {
this.setState({ active: false });
}
onShowPicker = ({ target }) => {
this.setState({ active: true });
this.setState({ placement: isMobile(window.innerWidth) ? 'bottom' : 'right' });
}
render() {
const { buttonId, label, value, onChange } = this.props;
const { active, placement } = this.state;
return (
<div className='label_input__color'>
<label>{label}</label>
<div id={buttonId} className='color-swatch' role='presentation' style={{ background: value }} title={value} value={value} onClick={this.onToggle} />
<Overlay show={active} placement={placement} target={this}>
<ColorPicker value={value} onChange={onChange} onClose={this.onHidePicker} />
</Overlay>
</div>
);
}
}
export class RadioItem extends ImmutablePureComponent {
static propTypes = {
@ -334,7 +236,7 @@ export class SelectDropdown extends ImmutablePureComponent {
<option key={item} value={item}>{items[item]}</option>
));
const selectElem = <select {...props}>{optionElems}</select>;
const selectElem = <div class='select-wrapper'><select {...props}>{optionElems}</select></div>;
return label ? (
<LabelInputContainer label={label} hint={hint}>{selectElem}</LabelInputContainer>

@ -0,0 +1,79 @@
import React from 'react';
import { connect } from 'react-redux';
import { injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
import {
SimpleInput,
SimpleForm,
FieldsGroup,
} from 'soapbox/features/forms';
export default @connect()
@injectIntl
class CSVImporter extends ImmutablePureComponent {
static propTypes = {
action: PropTypes.func.isRequired,
messages: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
state = {
file: null,
isLoading: false,
}
handleSubmit = (event) => {
const { dispatch, action } = this.props;
let params = new FormData();
params.append('list', this.state.file);
this.setState({ isLoading: true });
dispatch(action(params)).then(() => {
this.setState({ isLoading: false });
}).catch((error) => {
this.setState({ isLoading: false });
});
event.preventDefault();
}
handleFileChange = e => {
const [file] = e.target.files || [];
this.setState({ file });
}
render() {
const { intl, messages } = this.props;
return (
<SimpleForm onSubmit={this.handleSubmit}>
<fieldset disabled={this.state.isLoading}>
<FieldsGroup>
<div className='fields-row file-picker'>
<div className='fields-row__column fields-group fields-row__column-6'>
<SimpleInput
type='file'
accept={['.csv', 'text/csv']}
label={intl.formatMessage(messages.input_label)}
hint={intl.formatMessage(messages.input_hint)}
onChange={this.handleFileChange}
required
/>
</div>
</div>
</FieldsGroup>
</fieldset>
<div className='actions'>
<button name='button' type='submit' className='btn button button-primary'>
{intl.formatMessage(messages.submit)}
</button>
</div>
</SimpleForm>
);
}
}

@ -0,0 +1,56 @@
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
import Column from '../ui/components/column';
import {
importFollows,
importBlocks,
// importMutes,
} from 'soapbox/actions/import_data';
import CSVImporter from './components/csv_importer';
const messages = defineMessages({
heading: { id: 'column.import_data', defaultMessage: 'Import data' },
submit: { id: 'import_data.actions.import', defaultMessage: 'Import' },
});
const followMessages = defineMessages({
input_label: { id: 'import_data.follows_label', defaultMessage: 'Follows' },
input_hint: { id: 'import_data.hints.follows', defaultMessage: 'CSV file containing a list of followed accounts' },
submit: { id: 'import_data.actions.import_follows', defaultMessage: 'Import follows' },
});
const blockMessages = defineMessages({
input_label: { id: 'import_data.blocks_label', defaultMessage: 'Blocks' },
input_hint: { id: 'import_data.hints.blocks', defaultMessage: 'CSV file containing a list of blocked accounts' },
submit: { id: 'import_data.actions.import_blocks', defaultMessage: 'Import blocks' },
});
// Not yet supported by Pleroma stable, in develop branch
// const muteMessages = defineMessages({
// input_label: { id: 'import_data.mutes_label', defaultMessage: 'Mutes' },
// input_hint: { id: 'import_data.hints.mutes', defaultMessage: 'CSV file containing a list of muted accounts' },
// submit: { id: 'import_data.actions.import_mutes', defaultMessage: 'Import mutes' },
// });
export default @injectIntl
class ImportData extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
};
render() {
const { intl } = this.props;
return (
<Column icon='cloud-upload' heading={intl.formatMessage(messages.heading)} backBtnSlim>
<CSVImporter action={importFollows} messages={followMessages} />
<CSVImporter action={importBlocks} messages={blockMessages} />
{/* <CSVImporter action={importMutes} messages={muteMessages} /> */}
</Column>
);
}
}

@ -30,7 +30,7 @@ const getNotifications = createSelector([
state => getSettings(state).getIn(['notifications', 'quickFilter', 'show']),
state => getSettings(state).getIn(['notifications', 'quickFilter', 'active']),
state => ImmutableList(getSettings(state).getIn(['notifications', 'shows']).filter(item => !item).keys()),
state => state.getIn(['notifications', 'items']),
state => state.getIn(['notifications', 'items']).toList(),
], (showFilterBar, allowedType, excludedTypes, notifications) => {
if (!showFilterBar || allowedType === 'all') {
// used if user changed the notification settings after loading the notifications from the server

@ -185,6 +185,11 @@ class Preferences extends ImmutablePureComponent {
path={['dyslexicFont']}
/>
</div>
<SettingsCheckbox
label={<FormattedMessage id='preferences.fields.halloween_label' defaultMessage='Halloween mode' />}
hint={<FormattedMessage id='preferences.hints.halloween' defaultMessage='Beware: SPOOKY! Supports light/dark toggle.' />}
path={['halloween']}
/>
<SettingsCheckbox
label={<FormattedMessage id='preferences.fields.demetricator_label' defaultMessage='Use Demetricator' />}
hint={<FormattedMessage id='preferences.hints.demetricator' defaultMessage='Decrease social media anxiety by hiding all numbers from the site.' />}

@ -36,6 +36,12 @@ class Header extends ImmutablePureComponent {
this.handleSubmit = this.handleSubmit.bind(this);
}
state = {
isLoading: false,
mfa_auth_needed: false,
mfa_token: '',
}
getFormData = (form) => {
return Object.fromEntries(
Array.from(form).map(i => [i.name, i.value])
@ -71,14 +77,9 @@ class Header extends ImmutablePureComponent {
intl: PropTypes.object.isRequired,
}
state = {
mfa_auth_needed: false,
mfa_token: '',
}
render() {
const { me, instance, isLoading, intl } = this.props;
const { mfa_auth_needed, mfa_token } = this.state;
const { me, instance, intl } = this.props;
const { isLoading, mfa_auth_needed, mfa_token } = this.state;
return (
<nav className='header'>

@ -12,14 +12,18 @@ import {
Checkbox,
FileChooser,
SimpleTextarea,
ColorWithPicker,
FileChooserLogo,
FormPropTypes,
} from 'soapbox/features/forms';
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import { updateAdminConfig } from 'soapbox/actions/admin';
import Icon from 'soapbox/components/icon';
import { defaultConfig } from 'soapbox/actions/soapbox';
import { uploadMedia } from 'soapbox/actions/media';
import { SketchPicker } from 'react-color';
import Overlay from 'react-overlays/lib/Overlay';
import { isMobile } from 'soapbox/is_mobile';
import detectPassiveEvents from 'detect-passive-events';
const messages = defineMessages({
heading: { id: 'column.soapbox_config', defaultMessage: 'Soapbox config' },
@ -34,6 +38,8 @@ const messages = defineMessages({
rawJSONHint: { id: 'soapbox_config.raw_json_hint', defaultMessage: 'Advanced: Edit the settings data directly.' },
});
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
const templates = {
promoPanelItem: ImmutableMap({ icon: '', text: '', url: '' }),
footerItem: ImmutableMap({ title: '', url: '' }),
@ -314,7 +320,7 @@ class SoapboxConfig extends ImmutablePureComponent {
</div>
</div>
</div>
<div className='input with_block_label'>
{/* <div className='input with_block_label'>
<label><FormattedMessage id='soapbox_config.fields.custom_css_fields_label' defaultMessage='Custom CSS' /></label>
<span className='hint'>
<FormattedMessage id='soapbox_config.hints.custom_css_fields' defaultMessage='Insert a URL to a CSS file like `https://mysite.com/instance/custom.css`, or simply `/instance/custom.css`' />
@ -338,7 +344,7 @@ class SoapboxConfig extends ImmutablePureComponent {
<FormattedMessage id='soapbox_config.fields.custom_css.add' defaultMessage='Add another custom CSS URL' />
</div>
</div>
</div>
</div> */}
</FieldsGroup>
<FieldsGroup>
<div className={this.state.jsonValid ? 'code-editor' : 'code-editor code-editor--invalid'}>
@ -363,3 +369,95 @@ class SoapboxConfig extends ImmutablePureComponent {
}
}
class ColorPicker extends React.PureComponent {
static propTypes = {
style: PropTypes.object,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
onClose: PropTypes.func,
}
handleDocumentClick = e => {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose();
}
}
componentDidMount() {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleDocumentClick, false);
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
setRef = c => {
this.node = c;
}
render() {
const { style, value, onChange } = this.props;
let margin_left_picker = isMobile(window.innerWidth) ? '20px' : '12px';
return (
<div id='SketchPickerContainer' ref={this.setRef} style={{ ...style, marginLeft: margin_left_picker, position: 'absolute', zIndex: 1000 }}>
<SketchPicker color={value} disableAlpha onChange={onChange} />
</div>
);
}
}
class ColorWithPicker extends ImmutablePureComponent {
static propTypes = {
buttonId: PropTypes.string.isRequired,
label: FormPropTypes.label,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
}
onToggle = (e) => {
if (!e.key || e.key === 'Enter') {
if (this.state.active) {
this.onHidePicker();
} else {
this.onShowPicker(e);
}
}
}
state = {
active: false,
placement: null,
}
onHidePicker = () => {
this.setState({ active: false });
}
onShowPicker = ({ target }) => {
this.setState({ active: true });
this.setState({ placement: isMobile(window.innerWidth) ? 'bottom' : 'right' });
}
render() {
const { buttonId, label, value, onChange } = this.props;
const { active, placement } = this.state;
return (
<div className='label_input__color'>
<label>{label}</label>
<div id={buttonId} className='color-swatch' role='presentation' style={{ background: value }} title={value} value={value} onClick={this.onToggle} />
<Overlay show={active} placement={placement} target={this}>
<ColorPicker value={value} onChange={onChange} onClose={this.onHidePicker} />
</Overlay>
</div>
);
}
}

@ -1,20 +0,0 @@
import React from 'react';
import ComposeFormContainer from '../../compose/containers/compose_form_container';
import NotificationsContainer from '../../ui/containers/notifications_container';
import LoadingBarContainer from '../../ui/containers/loading_bar_container';
import ModalContainer from '../../ui/containers/modal_container';
export default class Compose extends React.PureComponent {
render() {
return (
<div>
<ComposeFormContainer />
<NotificationsContainer />
<ModalContainer />
<LoadingBarContainer className='loading-bar' />
</div>
);
}
}

@ -1,84 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { expandHashtagTimeline } from 'soapbox/actions/timelines';
import Masonry from 'react-masonry-infinite';
import { List as ImmutableList } from 'immutable';
import DetailedStatusContainer from 'soapbox/features/status/containers/detailed_status_container';
import { debounce } from 'lodash';
import LoadingIndicator from 'soapbox/components/loading_indicator';
const mapStateToProps = (state, { hashtag }) => ({
statusIds: state.getIn(['timelines', `hashtag:${hashtag}`, 'items'], ImmutableList()),
isLoading: state.getIn(['timelines', `hashtag:${hashtag}`, 'isLoading'], false),
hasMore: state.getIn(['timelines', `hashtag:${hashtag}`, 'hasMore'], false),
});
export default @connect(mapStateToProps)
class HashtagTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list.isRequired,
isLoading: PropTypes.bool.isRequired,
hasMore: PropTypes.bool.isRequired,
hashtag: PropTypes.string.isRequired,
};
componentDidMount() {
const { dispatch, hashtag } = this.props;
dispatch(expandHashtagTimeline(hashtag));
}
handleLoadMore = () => {
const maxId = this.props.statusIds.last();
if (maxId) {
this.props.dispatch(expandHashtagTimeline(this.props.hashtag, { maxId }));
}
}
setRef = c => {
this.masonry = c;
}
handleHeightChange = debounce(() => {
if (!this.masonry) {
return;
}
this.masonry.forcePack();
}, 50)
render() {
const { statusIds, hasMore, isLoading } = this.props;
const sizes = [
{ columns: 1, gutter: 0 },
{ mq: '415px', columns: 1, gutter: 10 },
{ mq: '640px', columns: 2, gutter: 10 },
{ mq: '960px', columns: 3, gutter: 10 },
{ mq: '1255px', columns: 3, gutter: 10 },
];
const loader = (isLoading && statusIds.isEmpty()) ? <LoadingIndicator key={0} /> : undefined;
return (
<Masonry ref={this.setRef} className='statuses-grid' hasMore={hasMore} loadMore={this.handleLoadMore} sizes={sizes} loader={loader}>
{statusIds.map(statusId => (
<div className='statuses-grid__item' key={statusId}>
<DetailedStatusContainer
id={statusId}
compact
measureHeight
onHeightChange={this.handleHeightChange}
/>
</div>
)).toArray()}
</Masonry>
);
}
}

@ -1,99 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { expandPublicTimeline, expandCommunityTimeline } from 'soapbox/actions/timelines';
import Masonry from 'react-masonry-infinite';
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import DetailedStatusContainer from 'soapbox/features/status/containers/detailed_status_container';
import { debounce } from 'lodash';
import LoadingIndicator from 'soapbox/components/loading_indicator';
const mapStateToProps = (state, { local }) => {
const timeline = state.getIn(['timelines', local ? 'community' : 'public'], ImmutableMap());
return {
statusIds: timeline.get('items', ImmutableList()),
isLoading: timeline.get('isLoading', false),
hasMore: timeline.get('hasMore', false),
};
};
export default @connect(mapStateToProps)
class PublicTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list.isRequired,
isLoading: PropTypes.bool.isRequired,
hasMore: PropTypes.bool.isRequired,
local: PropTypes.bool,
};
componentDidMount() {
this._connect();
}
componentDidUpdate(prevProps) {
if (prevProps.local !== this.props.local) {
this._connect();
}
}
_connect() {
const { dispatch, local } = this.props;
dispatch(local ? expandCommunityTimeline() : expandPublicTimeline());
}
handleLoadMore = () => {
const { dispatch, statusIds, local } = this.props;
const maxId = statusIds.last();
if (maxId) {
dispatch(local ? expandCommunityTimeline({ maxId }) : expandPublicTimeline({ maxId }));
}
}
setRef = c => {
this.masonry = c;
}
handleHeightChange = debounce(() => {
if (!this.masonry) {
return;
}
this.masonry.forcePack();
}, 50)
render() {
const { statusIds, hasMore, isLoading } = this.props;
const sizes = [
{ columns: 1, gutter: 0 },
{ mq: '415px', columns: 1, gutter: 10 },
{ mq: '640px', columns: 2, gutter: 10 },
{ mq: '960px', columns: 3, gutter: 10 },
{ mq: '1255px', columns: 3, gutter: 10 },
];
const loader = (isLoading && statusIds.isEmpty()) ? <LoadingIndicator key={0} /> : undefined;
return (
<Masonry ref={this.setRef} className='statuses-grid' hasMore={hasMore} loadMore={this.handleLoadMore} sizes={sizes} loader={loader}>
{statusIds.map(statusId => (
<div className='statuses-grid__item' key={statusId}>
<DetailedStatusContainer
id={statusId}
compact
measureHeight
onHeightChange={this.handleHeightChange}
/>
</div>
)).toArray()}
</Masonry>
);
}
}

@ -175,7 +175,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
</NavLink>
<DisplayName account={status.get('account')}>
<HoverRefWrapper accountId={status.getIn(['account', 'id'])}>
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} />
<NavLink className='floating-link' to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} />
</HoverRefWrapper>
</DisplayName>
</div>

@ -78,6 +78,7 @@ import {
Preferences,
EditProfile,
SoapboxConfig,
ImportData,
PasswordReset,
SecurityForm,
MfaForm,
@ -263,6 +264,7 @@ class SwitchingColumnsArea extends React.PureComponent {
<Redirect exact from='/settings' to='/settings/preferences' />
<WrappedRoute path='/settings/preferences' layout={LAYOUT.DEFAULT} component={Preferences} content={children} />
<WrappedRoute path='/settings/profile' layout={LAYOUT.DEFAULT} component={EditProfile} content={children} />
<WrappedRoute path='/settings/import' layout={LAYOUT.DEFAULT} component={ImportData} content={children} />
<WrappedRoute path='/soapbox/config' layout={LAYOUT.DEFAULT} component={SoapboxConfig} content={children} />
<WrappedRoute layout={LAYOUT.EMPTY} component={GenericNotFound} content={children} />

@ -186,6 +186,10 @@ export function SoapboxConfig() {
return import(/* webpackChunkName: "features/soapbox_config" */'../../soapbox_config');
}
export function ImportData() {
return import(/* webpackChunkName: "features/import_data" */'../../import_data');
}
export function PasswordReset() {
return import(/* webpackChunkName: "features/auth_login" */'../../auth_login/components/password_reset');
}

@ -18,7 +18,7 @@ class WrappedRoute extends React.Component {
static propTypes = {
component: PropTypes.func.isRequired,
page: PropTypes.func,
page: PropTypes.object,
content: PropTypes.node,
componentParams: PropTypes.object,
layout: PropTypes.object,

@ -1,6 +0,0 @@
'use strict';
const element = document.getElementById('initial-state');
const initialState = element ? JSON.parse(element.textContent) : {};
export default initialState;

@ -3,18 +3,18 @@ import { Map as ImmutableMap } from 'immutable';
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from 'soapbox/actions/me';
import { SETTING_CHANGE } from 'soapbox/actions/settings';
import * as actions from 'soapbox/actions/compose';
//import { STORE_HYDRATE } from 'soapbox/actions/store';
//import { REDRAFT } from 'soapbox/actions/statuses';
import { TIMELINE_DELETE } from 'soapbox/actions/timelines';
describe('compose reducer', () => {
it('returns the initial state by default', () => {
expect(reducer(undefined, {}).toJS()).toMatchObject({
const state = reducer(undefined, {});
expect(state.toJS()).toMatchObject({
mounted: 0,
sensitive: false,
spoiler: false,
spoiler_text: '',
privacy: null,
privacy: 'public',
text: '',
focusDate: null,
caretPosition: null,
@ -30,10 +30,10 @@ describe('compose reducer', () => {
suggestions: [],
default_privacy: 'public',
default_sensitive: false,
idempotencyKey: null,
tagHistory: [],
content_type: 'text/markdown',
});
expect(state.get('idempotencyKey').length === 36);
});
it('uses \'public\' scope as default', () => {
@ -132,23 +132,6 @@ describe('compose reducer', () => {
});
});
// it('should handle STORE_HYDRATE', () => {
// const state = ImmutableMap({ });
// const action = {
// type: STORE_HYDRATE,
// state: ImmutableMap({
// compose: true,
// text: 'newtext',
// }),
// };
// expect(reducer(state, action)).toEqual(ImmutableMap({
// state: ImmutableMap({
// compose: true,
// text: 'newtext',
// }),
// }));
// });
it('should handle COMPOSE_MOUNT', () => {
const state = ImmutableMap({ mounted: 1 });
const action = {

@ -1,23 +1,23 @@
import * as actions from 'soapbox/actions/notifications';
import reducer from '../notifications';
import notifications from 'soapbox/__fixtures__/notifications.json';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { Map as ImmutableMap, OrderedMap as ImmutableOrderedMap } from 'immutable';
import { take } from 'lodash';
import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'soapbox/actions/accounts';
import notification from 'soapbox/__fixtures__/notification.json';
import intlMessages from 'soapbox/__fixtures__/intlMessages.json';
import relationship from 'soapbox/__fixtures__/relationship.json';
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from 'soapbox/actions/timelines';
import { TIMELINE_DELETE } from 'soapbox/actions/timelines';
describe('notifications reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual(ImmutableMap({
items: ImmutableList(),
items: ImmutableOrderedMap(),
hasMore: true,
top: false,
unread: 0,
isLoading: false,
queuedNotifications: ImmutableList(),
queuedNotifications: ImmutableOrderedMap(),
totalQueuedNotificationsCount: 0,
lastRead: -1,
}));
@ -32,8 +32,8 @@ describe('notifications reducer', () => {
skipLoading: true,
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList([
ImmutableMap({
items: ImmutableOrderedMap([
['10744', ImmutableMap({
id: '10744',
type: 'pleroma:emoji_reaction',
account: '9vMAje101ngtjlMj7w',
@ -42,8 +42,8 @@ describe('notifications reducer', () => {
emoji: '😢',
chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
})],
['10743', ImmutableMap({
id: '10743',
type: 'favourite',
account: '9v5c6xSEgAi3Zu1Lv6',
@ -52,8 +52,8 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
})],
['10741', ImmutableMap({
id: '10741',
type: 'favourite',
account: '9v5cKMOPGqPcgfcWp6',
@ -62,13 +62,13 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
})],
]),
hasMore: false,
top: false,
unread: 1,
isLoading: false,
queuedNotifications: ImmutableList(),
queuedNotifications: ImmutableOrderedMap(),
totalQueuedNotificationsCount: 0,
lastRead: -1,
}));
@ -100,8 +100,8 @@ describe('notifications reducer', () => {
it('should handle NOTIFICATIONS_FILTER_SET', () => {
const state = ImmutableMap({
items: ImmutableList([
ImmutableMap({
items: ImmutableOrderedMap([
['10744', ImmutableMap({
id: '10744',
type: 'pleroma:emoji_reaction',
account: '9vMAje101ngtjlMj7w',
@ -110,8 +110,8 @@ describe('notifications reducer', () => {
emoji: '😢',
chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
})],
['10743', ImmutableMap({
id: '10743',
type: 'favourite',
account: '9v5c6xSEgAi3Zu1Lv6',
@ -120,8 +120,8 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
})],
['10741', ImmutableMap({
id: '10741',
type: 'favourite',
account: '9v5cKMOPGqPcgfcWp6',
@ -130,13 +130,13 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
})],
]),
hasMore: false,
top: false,
unread: 1,
isLoading: false,
queuedNotifications: ImmutableList(),
queuedNotifications: ImmutableOrderedMap(),
totalQueuedNotificationsCount: 0,
lastRead: -1,
});
@ -144,12 +144,12 @@ describe('notifications reducer', () => {
type: actions.NOTIFICATIONS_FILTER_SET,
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList(),
items: ImmutableOrderedMap(),
hasMore: true,
top: false,
unread: 1,
isLoading: false,
queuedNotifications: ImmutableList(),
queuedNotifications: ImmutableOrderedMap(),
totalQueuedNotificationsCount: 0,
lastRead: -1,
}));
@ -185,7 +185,7 @@ describe('notifications reducer', () => {
it('should handle NOTIFICATIONS_UPDATE, when top = false, increment unread', () => {
const state = ImmutableMap({
items: ImmutableList(),
items: ImmutableOrderedMap(),
top: false,
unread: 1,
});
@ -194,8 +194,8 @@ describe('notifications reducer', () => {
notification: notification,
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList([
ImmutableMap({
items: ImmutableOrderedMap([
['10743', ImmutableMap({
id: '10743',
type: 'favourite',
account: '9v5c6xSEgAi3Zu1Lv6',
@ -204,7 +204,7 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
})],
]),
top: false,
unread: 2,
@ -213,8 +213,8 @@ describe('notifications reducer', () => {
it('should handle NOTIFICATIONS_UPDATE_QUEUE', () => {
const state = ImmutableMap({
items: ImmutableList([]),
queuedNotifications: ImmutableList([]),
items: ImmutableOrderedMap(),
queuedNotifications: ImmutableOrderedMap(),
totalQueuedNotificationsCount: 0,
});
const action = {
@ -224,19 +224,19 @@ describe('notifications reducer', () => {
intlLocale: 'en',
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList([]),
queuedNotifications: ImmutableList([{
items: ImmutableOrderedMap(),
queuedNotifications: ImmutableOrderedMap([[notification.id, {
notification: notification,
intlMessages: intlMessages,
intlLocale: 'en',
}]),
}]]),
totalQueuedNotificationsCount: 1,
}));
});
it('should handle NOTIFICATIONS_DEQUEUE', () => {
const state = ImmutableMap({
items: ImmutableList([]),
items: ImmutableOrderedMap(),
queuedNotifications: take(notifications, 1),
totalQueuedNotificationsCount: 1,
});
@ -244,16 +244,16 @@ describe('notifications reducer', () => {
type: actions.NOTIFICATIONS_DEQUEUE,
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList([]),
queuedNotifications: ImmutableList([]),
items: ImmutableOrderedMap(),
queuedNotifications: ImmutableOrderedMap(),
totalQueuedNotificationsCount: 0,
}));
});
it('should handle NOTIFICATIONS_EXPAND_SUCCESS with non-empty items and next set true', () => {
const state = ImmutableMap({
items: ImmutableList([
ImmutableMap({
items: ImmutableOrderedMap([
['10734', ImmutableMap({
id: '10734',
type: 'pleroma:emoji_reaction',
account: '9vMAje101ngtjlMj7w',
@ -262,7 +262,7 @@ describe('notifications reducer', () => {
emoji: '😢',
chat_message: undefined,
is_seen: false,
}),
})],
]),
unread: 1,
hasMore: true,
@ -274,8 +274,8 @@ describe('notifications reducer', () => {
next: true,
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList([
ImmutableMap({
items: ImmutableOrderedMap([
['10744', ImmutableMap({
id: '10744',
type: 'pleroma:emoji_reaction',
account: '9vMAje101ngtjlMj7w',
@ -284,8 +284,8 @@ describe('notifications reducer', () => {
emoji: '😢',
chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
})],
['10743', ImmutableMap({
id: '10743',
type: 'favourite',
account: '9v5c6xSEgAi3Zu1Lv6',
@ -294,8 +294,8 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
})],
['10741', ImmutableMap({
id: '10741',
type: 'favourite',
account: '9v5cKMOPGqPcgfcWp6',
@ -304,8 +304,8 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
})],
['10734', ImmutableMap({
id: '10734',
type: 'pleroma:emoji_reaction',
account: '9vMAje101ngtjlMj7w',
@ -314,7 +314,7 @@ describe('notifications reducer', () => {
emoji: '😢',
chat_message: undefined,
is_seen: false,
}),
})],
]),
unread: 1,
hasMore: true,
@ -324,7 +324,7 @@ describe('notifications reducer', () => {
it('should handle NOTIFICATIONS_EXPAND_SUCCESS with empty items and next set true', () => {
const state = ImmutableMap({
items: ImmutableList([]),
items: ImmutableOrderedMap(),
unread: 1,
hasMore: true,
isLoading: false,
@ -335,8 +335,8 @@ describe('notifications reducer', () => {
next: true,
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList([
ImmutableMap({
items: ImmutableOrderedMap([
['10744', ImmutableMap({
id: '10744',
type: 'pleroma:emoji_reaction',
account: '9vMAje101ngtjlMj7w',
@ -345,8 +345,8 @@ describe('notifications reducer', () => {
emoji: '😢',
chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
})],
['10743', ImmutableMap({
id: '10743',
type: 'favourite',
account: '9v5c6xSEgAi3Zu1Lv6',
@ -355,8 +355,8 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
})],
['10741', ImmutableMap({
id: '10741',
type: 'favourite',
account: '9v5cKMOPGqPcgfcWp6',
@ -365,7 +365,7 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
})],
]),
unread: 1,
hasMore: true,
@ -375,8 +375,8 @@ describe('notifications reducer', () => {
it('should handle ACCOUNT_BLOCK_SUCCESS', () => {
const state = ImmutableMap({
items: ImmutableList([
ImmutableMap({
items: ImmutableOrderedMap([
['10744', ImmutableMap({
id: '10744',
type: 'pleroma:emoji_reaction',
account: '9vMAje101ngtjlMj7w',
@ -385,8 +385,8 @@ describe('notifications reducer', () => {
emoji: '😢',
chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
})],
['10743', ImmutableMap({
id: '10743',
type: 'favourite',
account: '9v5c6xSEgAi3Zu1Lv6',
@ -395,8 +395,8 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
})],
['10741', ImmutableMap({
id: '10741',
type: 'favourite',
account: '9v5cKMOPGqPcgfcWp6',
@ -405,7 +405,7 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
})],
]),
});
const action = {
@ -413,8 +413,8 @@ describe('notifications reducer', () => {
relationship: relationship,
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList([
ImmutableMap({
items: ImmutableOrderedMap([
['10743', ImmutableMap({
id: '10743',
type: 'favourite',
account: '9v5c6xSEgAi3Zu1Lv6',
@ -423,8 +423,8 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
})],
['10741', ImmutableMap({
id: '10741',
type: 'favourite',
account: '9v5cKMOPGqPcgfcWp6',
@ -433,15 +433,15 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
})],
]),
}));
});
it('should handle ACCOUNT_MUTE_SUCCESS', () => {
const state = ImmutableMap({
items: ImmutableList([
ImmutableMap({
items: ImmutableOrderedMap([
['10744', ImmutableMap({
id: '10744',
type: 'pleroma:emoji_reaction',
account: '9vMAje101ngtjlMj7w',
@ -450,8 +450,8 @@ describe('notifications reducer', () => {
emoji: '😢',
chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
})],
['10743', ImmutableMap({
id: '10743',
type: 'favourite',
account: '9v5c6xSEgAi3Zu1Lv6',
@ -460,8 +460,8 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
})],
['10741', ImmutableMap({
id: '10741',
type: 'favourite',
account: '9v5cKMOPGqPcgfcWp6',
@ -470,7 +470,7 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
})],
]),
});
const action = {
@ -478,8 +478,8 @@ describe('notifications reducer', () => {
relationship: relationship,
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList([
ImmutableMap({
items: ImmutableOrderedMap([
['10743', ImmutableMap({
id: '10743',
type: 'favourite',
account: '9v5c6xSEgAi3Zu1Lv6',
@ -488,8 +488,8 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
})],
['10741', ImmutableMap({
id: '10741',
type: 'favourite',
account: '9v5cKMOPGqPcgfcWp6',
@ -498,43 +498,43 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
})],
]),
}));
});
it('should handle NOTIFICATIONS_CLEAR', () => {
const state = ImmutableMap({
items: ImmutableList([]),
items: ImmutableOrderedMap(),
hasMore: true,
});
const action = {
type: actions.NOTIFICATIONS_CLEAR,
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList([]),
items: ImmutableOrderedMap(),
hasMore: false,
}));
});
it('should handle NOTIFICATIONS_MARK_READ_REQUEST', () => {
const state = ImmutableMap({
items: ImmutableList([]),
items: ImmutableOrderedMap(),
});
const action = {
type: actions.NOTIFICATIONS_MARK_READ_REQUEST,
lastRead: 35098814,
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList([]),
items: ImmutableOrderedMap(),
lastRead: 35098814,
}));
});
it('should handle TIMELINE_DELETE', () => {
const state = ImmutableMap({
items: ImmutableList([
ImmutableMap({
items: ImmutableOrderedMap([
['10744', ImmutableMap({
id: '10744',
type: 'pleroma:emoji_reaction',
account: '9vMAje101ngtjlMj7w',
@ -543,8 +543,8 @@ describe('notifications reducer', () => {
emoji: '😢',
chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
})],
['10743', ImmutableMap({
id: '10743',
type: 'favourite',
account: '9v5c6xSEgAi3Zu1Lv6',
@ -553,8 +553,8 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
})],
['10741', ImmutableMap({
id: '10741',
type: 'favourite',
account: '9v5cKMOPGqPcgfcWp6',
@ -563,7 +563,7 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
})],
]),
});
const action = {
@ -571,84 +571,87 @@ describe('notifications reducer', () => {
id: '9vvNxoo5EFbbnfdXQu',
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList([]),
items: ImmutableOrderedMap(),
}));
});
it('should handle TIMELINE_DISCONNECT', () => {
const state = ImmutableMap({
items: ImmutableList([
ImmutableMap({
id: '10744',
type: 'pleroma:emoji_reaction',
account: '9vMAje101ngtjlMj7w',
created_at: '2020-06-10T02:54:39.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: '😢',
chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
id: '10743',
type: 'favourite',
account: '9v5c6xSEgAi3Zu1Lv6',
created_at: '2020-06-10T02:51:05.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
id: '10741',
type: 'favourite',
account: '9v5cKMOPGqPcgfcWp6',
created_at: '2020-06-10T02:05:06.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
]),
});
const action = {
type: TIMELINE_DISCONNECT,
timeline: 'home',
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList([
null,
ImmutableMap({
id: '10744',
type: 'pleroma:emoji_reaction',
account: '9vMAje101ngtjlMj7w',
created_at: '2020-06-10T02:54:39.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: '😢',
chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
id: '10743',
type: 'favourite',
account: '9v5c6xSEgAi3Zu1Lv6',
created_at: '2020-06-10T02:51:05.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
id: '10741',
type: 'favourite',
account: '9v5cKMOPGqPcgfcWp6',
created_at: '2020-06-10T02:05:06.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
]),
}));
});
// Disable for now
// https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/432
//
// it('should handle TIMELINE_DISCONNECT', () => {
// const state = ImmutableMap({
// items: ImmutableOrderedSet([
// ImmutableMap({
// id: '10744',
// type: 'pleroma:emoji_reaction',
// account: '9vMAje101ngtjlMj7w',
// created_at: '2020-06-10T02:54:39.000Z',
// status: '9vvNxoo5EFbbnfdXQu',
// emoji: '😢',
// chat_message: undefined,
// is_seen: false,
// }),
// ImmutableMap({
// id: '10743',
// type: 'favourite',
// account: '9v5c6xSEgAi3Zu1Lv6',
// created_at: '2020-06-10T02:51:05.000Z',
// status: '9vvNxoo5EFbbnfdXQu',
// emoji: undefined,
// chat_message: undefined,
// is_seen: true,
// }),
// ImmutableMap({
// id: '10741',
// type: 'favourite',
// account: '9v5cKMOPGqPcgfcWp6',
// created_at: '2020-06-10T02:05:06.000Z',
// status: '9vvNxoo5EFbbnfdXQu',
// emoji: undefined,
// chat_message: undefined,
// is_seen: true,
// }),
// ]),
// });
// const action = {
// type: TIMELINE_DISCONNECT,
// timeline: 'home',
// };
// expect(reducer(state, action)).toEqual(ImmutableMap({
// items: ImmutableOrderedSet([
// null,
// ImmutableMap({
// id: '10744',
// type: 'pleroma:emoji_reaction',
// account: '9vMAje101ngtjlMj7w',
// created_at: '2020-06-10T02:54:39.000Z',
// status: '9vvNxoo5EFbbnfdXQu',
// emoji: '😢',
// chat_message: undefined,
// is_seen: false,
// }),
// ImmutableMap({
// id: '10743',
// type: 'favourite',
// account: '9v5c6xSEgAi3Zu1Lv6',
// created_at: '2020-06-10T02:51:05.000Z',
// status: '9vvNxoo5EFbbnfdXQu',
// emoji: undefined,
// chat_message: undefined,
// is_seen: true,
// }),
// ImmutableMap({
// id: '10741',
// type: 'favourite',
// account: '9v5cKMOPGqPcgfcWp6',
// created_at: '2020-06-10T02:05:06.000Z',
// status: '9vvNxoo5EFbbnfdXQu',
// emoji: undefined,
// chat_message: undefined,
// is_seen: true,
// }),
// ]),
// }));
// });
});

@ -3,6 +3,7 @@ import {
CHAT_MESSAGES_FETCH_SUCCESS,
CHAT_MESSAGE_SEND_REQUEST,
CHAT_MESSAGE_SEND_SUCCESS,
CHAT_MESSAGE_DELETE_SUCCESS,
} from 'soapbox/actions/chats';
import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming';
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
@ -59,6 +60,8 @@ export default function chatMessageLists(state = initialState, action) {
return updateList(state, action.chatId, action.chatMessages.map(chat => chat.id));
case CHAT_MESSAGE_SEND_SUCCESS:
return replaceMessage(state, action.chatId, action.uuid, action.chatMessage.id);
case CHAT_MESSAGE_DELETE_SUCCESS:
return state.update(action.chatId, chat => chat.delete(action.messageId));
default:
return state;
}

@ -3,6 +3,8 @@ import {
CHAT_MESSAGES_FETCH_SUCCESS,
CHAT_MESSAGE_SEND_REQUEST,
CHAT_MESSAGE_SEND_SUCCESS,
CHAT_MESSAGE_DELETE_REQUEST,
CHAT_MESSAGE_DELETE_SUCCESS,
} from 'soapbox/actions/chats';
import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming';
import { Map as ImmutableMap, fromJS } from 'immutable';
@ -43,6 +45,11 @@ export default function chatMessages(state = initialState, action) {
return importMessage(state, fromJS(action.chatMessage)).delete(action.uuid);
case STREAMING_CHAT_UPDATE:
return importLastMessages(state, fromJS([action.chat]));
case CHAT_MESSAGE_DELETE_REQUEST:
return state.update(action.messageId, chatMessage =>
chatMessage.set('pending', true).set('deleting', true));
case CHAT_MESSAGE_DELETE_SUCCESS:
return state.delete(action.messageId);
default:
return state;
}

@ -38,21 +38,22 @@ import {
COMPOSE_POLL_SETTINGS_CHANGE,
} from '../actions/compose';
import { TIMELINE_DELETE } from '../actions/timelines';
import { STORE_HYDRATE } from '../actions/store';
import { REDRAFT } from '../actions/statuses';
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from '../actions/me';
import { SETTING_CHANGE, FE_NAME } from '../actions/settings';
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import { tagHistory } from 'soapbox/settings';
import uuid from '../uuid';
import { unescapeHTML } from '../utils/html';
const initialState = ImmutableMap({
id: null,
mounted: 0,
sensitive: false,
spoiler: false,
spoiler_text: '',
content_type: 'text/markdown',
privacy: null,
privacy: 'public',
text: '',
focusDate: null,
caretPosition: null,
@ -69,7 +70,7 @@ const initialState = ImmutableMap({
default_privacy: 'public',
default_sensitive: false,
resetFileKey: Math.floor((Math.random() * 0x10000)),
idempotencyKey: null,
idempotencyKey: uuid(),
tagHistory: ImmutableList(),
});
@ -178,16 +179,6 @@ const privacyPreference = (a, b) => {
return order[Math.max(order.indexOf(a), order.indexOf(b), 0)];
};
const hydrate = (state, hydratedState = ImmutableMap()) => {
state = clearAll(state.merge(hydratedState));
if (hydratedState.has('text')) {
state = state.set('text', hydratedState.get('text'));
}
return state;
};
const domParser = new DOMParser();
const expandMentions = status => {
@ -204,8 +195,6 @@ const expandMentions = status => {
export default function compose(state = initialState, action) {
let me, defaultPrivacy;
switch(action.type) {
case STORE_HYDRATE:
return hydrate(state, action.state.get('compose'));
case COMPOSE_MOUNT:
return state.set('mounted', state.get('mounted') + 1);
case COMPOSE_UNMOUNT:
@ -374,9 +363,12 @@ export default function compose(state = initialState, action) {
return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
case ME_FETCH_SUCCESS:
me = fromJS(action.me);
defaultPrivacy = me.getIn(['pleroma', 'settings_store', FE_NAME, 'defaultPrivacy']);
if (!defaultPrivacy) return state;
return state.set('default_privacy', defaultPrivacy).set('privacy', defaultPrivacy);
defaultPrivacy = me.getIn(['pleroma', 'settings_store', FE_NAME, 'defaultPrivacy'], 'public');
return state.merge({
default_privacy: defaultPrivacy,
privacy: defaultPrivacy,
tagHistory: ImmutableList(tagHistory.get(action.me.id)),
});
case ME_PATCH_SUCCESS:
me = fromJS(action.me);
defaultPrivacy = me.getIn(['pleroma', 'settings_store', FE_NAME, 'defaultPrivacy']);

@ -3,7 +3,8 @@ import {
ACCOUNT_MUTE_SUCCESS,
} from '../actions/accounts';
import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses';
import { TIMELINE_DELETE, TIMELINE_UPDATE } from '../actions/timelines';
import { TIMELINE_DELETE } from '../actions/timelines';
import { STATUS_IMPORT, STATUSES_IMPORT } from 'soapbox/actions/importer';
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
const initialState = ImmutableMap({
@ -87,8 +88,12 @@ export default function replies(state = initialState, action) {
return normalizeContext(state, action.id, action.ancestors, action.descendants);
case TIMELINE_DELETE:
return deleteFromContexts(state, [action.id]);
case TIMELINE_UPDATE:
case STATUS_IMPORT:
return updateContext(state, action.status);
case STATUSES_IMPORT:
return state.withMutations(mutable =>
action.statuses.forEach(status => updateContext(mutable, status)));
default:
return state;
}

@ -1,4 +1,6 @@
import { combineReducers } from 'redux-immutable';
import { Map as ImmutableMap } from 'immutable';
import { AUTH_LOGGED_OUT } from 'soapbox/actions/auth';
import dropdown_menu from './dropdown_menu';
import timelines from './timelines';
import meta from './meta';
@ -48,7 +50,7 @@ import chat_messages from './chat_messages';
import chat_message_lists from './chat_message_lists';
import profile_hover_card from './profile_hover_card';
const reducers = {
const appReducer = combineReducers({
dropdown_menu,
timelines,
meta,
@ -97,6 +99,27 @@ const reducers = {
chat_messages,
chat_message_lists,
profile_hover_card,
});
// Clear the state (mostly) when the user logs out
const logOut = (state = ImmutableMap()) => {
const whitelist = ['instance', 'soapbox', 'custom_emojis'];
return ImmutableMap(
whitelist.reduce((acc, curr) => {
acc[curr] = state.get(curr);
return acc;
}, {})
);
};
const rootReducer = (state, action) => {
switch(action.type) {
case AUTH_LOGGED_OUT:
return appReducer(logOut(state), action);
default:
return appReducer(state, action);
}
};
export default combineReducers(reducers);
export default rootReducer;

@ -1,4 +1,3 @@
import { STORE_HYDRATE } from '../actions/store';
import {
Map as ImmutableMap,
List as ImmutableList,
@ -35,8 +34,6 @@ const initialState = ImmutableMap({
export default function meta(state = initialState, action) {
switch(action.type) {
case STORE_HYDRATE:
return state.merge(action.state.get('media_attachments'));
default:
return state;
}

@ -1,6 +1,5 @@
'use strict';
import { STORE_HYDRATE } from '../actions/store';
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from 'soapbox/actions/me';
import { Map as ImmutableMap, fromJS } from 'immutable';
@ -8,8 +7,6 @@ const initialState = ImmutableMap();
export default function meta(state = initialState, action) {
switch(action.type) {
case STORE_HYDRATE:
return state.merge(action.state.get('meta'));
case ME_FETCH_SUCCESS:
case ME_PATCH_SUCCESS:
const me = fromJS(action.me);

@ -15,22 +15,29 @@ import {
ACCOUNT_BLOCK_SUCCESS,
ACCOUNT_MUTE_SUCCESS,
} from '../actions/accounts';
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import compareId from '../compare_id';
import { TIMELINE_DELETE } from '../actions/timelines';
import { Map as ImmutableMap, OrderedMap as ImmutableOrderedMap } from 'immutable';
import { get } from 'lodash';
const initialState = ImmutableMap({
items: ImmutableList(),
items: ImmutableOrderedMap(),
hasMore: true,
top: false,
unread: 0,
isLoading: false,
queuedNotifications: ImmutableList(), //max = MAX_QUEUED_NOTIFICATIONS
queuedNotifications: ImmutableOrderedMap(), //max = MAX_QUEUED_NOTIFICATIONS
totalQueuedNotificationsCount: 0, //used for queuedItems overflow for MAX_QUEUED_NOTIFICATIONS+
lastRead: -1,
});
// For sorting the notifications
const comparator = (a, b) => {
const parse = m => parseInt(m.get('id'), 10);
if (parse(a) < parse(b)) return 1;
if (parse(a) > parse(b)) return -1;
return 0;
};
const notificationToMap = notification => ImmutableMap({
id: notification.id,
type: notification.type,
@ -42,86 +49,67 @@ const notificationToMap = notification => ImmutableMap({
is_seen: get(notification, ['pleroma', 'is_seen'], true),
});
// https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/424
const isValid = notification => Boolean(notification.account.id);
const normalizeNotification = (state, notification) => {
const top = state.get('top');
if (!top) {
state = state.update('unread', unread => unread + 1);
}
if (!top) state = state.update('unread', unread => unread + 1);
return state.update('items', list => {
if (top && list.size > 40) {
list = list.take(20);
return state.update('items', map => {
if (top && map.size > 40) {
map = map.take(20);
}
return list.unshift(notificationToMap(notification));
return map.set(notification.id, notificationToMap(notification)).sort(comparator);
});
};
const expandNormalizedNotifications = (state, notifications, next) => {
let items = ImmutableList();
const processRawNotifications = notifications => (
ImmutableOrderedMap(
notifications
.filter(isValid)
.map(n => [n.id, notificationToMap(n)])
));
notifications.forEach((n) => {
if (!n.account.id) return;
items = items.push(notificationToMap(n));
});
const expandNormalizedNotifications = (state, notifications, next) => {
const items = processRawNotifications(notifications);
return state.withMutations(mutable => {
if (!items.isEmpty()) {
mutable.update('items', list => {
const lastIndex = 1 + list.findLastIndex(
item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id'))
);
const firstIndex = 1 + list.take(lastIndex).findLastIndex(
item => item !== null && compareId(item.get('id'), items.first().get('id')) > 0
);
return list.take(firstIndex).concat(items, list.skip(lastIndex));
});
}
if (!next) {
mutable.set('hasMore', false);
}
mutable.update('items', map => map.merge(items).sort(comparator));
if (!next) mutable.set('hasMore', false);
mutable.set('isLoading', false);
});
};
const filterNotifications = (state, relationship) => {
return state.update('items', list => list.filterNot(item => item !== null && item.get('account') === relationship.id));
return state.update('items', map => map.filterNot(item => item !== null && item.get('account') === relationship.id));
};
const updateTop = (state, top) => {
if (top) {
state = state.set('unread', 0);
}
if (top) state = state.set('unread', 0);
return state.set('top', top);
};
const deleteByStatus = (state, statusId) => {
return state.update('items', list => list.filterNot(item => item !== null && item.get('status') === statusId));
return state.update('items', map => map.filterNot(item => item !== null && item.get('status') === statusId));
};
const updateNotificationsQueue = (state, notification, intlMessages, intlLocale) => {
const queuedNotifications = state.getIn(['queuedNotifications'], ImmutableList());
const listedNotifications = state.getIn(['items'], ImmutableList());
const queuedNotifications = state.getIn(['queuedNotifications'], ImmutableOrderedMap());
const listedNotifications = state.getIn(['items'], ImmutableOrderedMap());
const totalQueuedNotificationsCount = state.getIn(['totalQueuedNotificationsCount'], 0);
let alreadyExists = queuedNotifications.find(existingQueuedNotification => existingQueuedNotification.id === notification.id);
if (!alreadyExists) alreadyExists = listedNotifications.find(existingListedNotification => existingListedNotification.get('id') === notification.id);
if (alreadyExists) {
return state;
}
const alreadyExists = queuedNotifications.has(notification.id) || listedNotifications.has(notification.id);
if (alreadyExists) return state;
let newQueuedNotifications = queuedNotifications;
return state.withMutations(mutable => {
if (totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) {
mutable.set('queuedNotifications', newQueuedNotifications.push({
mutable.set('queuedNotifications', newQueuedNotifications.set(notification.id, {
notification,
intlMessages,
intlLocale,
@ -131,6 +119,9 @@ const updateNotificationsQueue = (state, notification, intlMessages, intlLocale)
});
};
const countUnseen = notifications => notifications.reduce((acc, cur) =>
get(cur, ['pleroma', 'is_seen'], false) === false ? acc + 1 : acc, 0);
export default function notifications(state = initialState, action) {
switch(action.type) {
case NOTIFICATIONS_EXPAND_REQUEST:
@ -138,7 +129,7 @@ export default function notifications(state = initialState, action) {
case NOTIFICATIONS_EXPAND_FAIL:
return state.set('isLoading', false);
case NOTIFICATIONS_FILTER_SET:
return state.set('items', ImmutableList()).set('hasMore', true);
return state.set('items', ImmutableOrderedMap()).set('hasMore', true);
case NOTIFICATIONS_SCROLL_TOP:
return updateTop(state, action.top);
case NOTIFICATIONS_UPDATE:
@ -147,12 +138,11 @@ export default function notifications(state = initialState, action) {
return updateNotificationsQueue(state, action.notification, action.intlMessages, action.intlLocale);
case NOTIFICATIONS_DEQUEUE:
return state.withMutations(mutable => {
mutable.set('queuedNotifications', ImmutableList());
mutable.set('queuedNotifications', ImmutableOrderedMap());
mutable.set('totalQueuedNotificationsCount', 0);
});
case NOTIFICATIONS_EXPAND_SUCCESS:
const legacyUnread = action.notifications.reduce((acc, cur) =>
get(cur, ['pleroma', 'is_seen'], false) === false ? acc + 1 : acc, 0);
const legacyUnread = countUnseen(action.notifications);
return expandNormalizedNotifications(state, action.notifications, action.next)
.merge({ unread: Math.max(legacyUnread, state.get('unread')) });
case ACCOUNT_BLOCK_SUCCESS:
@ -160,15 +150,21 @@ export default function notifications(state = initialState, action) {
case ACCOUNT_MUTE_SUCCESS:
return action.relationship.muting_notifications ? filterNotifications(state, action.relationship) : state;
case NOTIFICATIONS_CLEAR:
return state.set('items', ImmutableList()).set('hasMore', false);
return state.set('items', ImmutableOrderedMap()).set('hasMore', false);
case NOTIFICATIONS_MARK_READ_REQUEST:
return state.set('lastRead', action.lastRead);
case TIMELINE_DELETE:
return deleteByStatus(state, action.id);
case TIMELINE_DISCONNECT:
return action.timeline === 'home' ?
state.update('items', items => items.first() ? items.unshift(null) : items) :
state;
// Disable for now
// https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/432
//
// case TIMELINE_DISCONNECT:
// // This is kind of a hack - `null` renders a LoadGap in the component
// // https://github.com/tootsuite/mastodon/pull/6886
// return action.timeline === 'home' ?
// state.update('items', items => items.first() ? ImmutableOrderedSet([null]).union(items) : items) :
// state;
default:
return state;
}

@ -1,4 +1,3 @@
import { STORE_HYDRATE } from '../actions/store';
import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, SET_ALERTS } from '../actions/push_notifications';
import Immutable from 'immutable';
@ -17,21 +16,6 @@ const initialState = Immutable.Map({
export default function push_subscriptions(state = initialState, action) {
switch(action.type) {
case STORE_HYDRATE: {
const push_subscription = action.state.get('push_subscription');
if (push_subscription) {
return state
.set('subscription', new Immutable.Map({
id: push_subscription.get('id'),
endpoint: push_subscription.get('endpoint'),
}))
.set('alerts', push_subscription.get('alerts') || initialState.get('alerts'))
.set('isSubscribed', true);
}
return state;
}
case SET_SUBSCRIPTION:
return state
.set('subscription', new Immutable.Map({

@ -1,11 +1,9 @@
import { SETTING_CHANGE, SETTING_SAVE, FE_NAME } from '../actions/settings';
import { NOTIFICATIONS_FILTER_SET } from '../actions/notifications';
import { STORE_HYDRATE } from '../actions/store';
import { EMOJI_USE } from '../actions/emojis';
import { LIST_DELETE_SUCCESS, LIST_FETCH_FAIL } from '../actions/lists';
import { ME_FETCH_SUCCESS } from 'soapbox/actions/me';
import { Map as ImmutableMap, fromJS } from 'immutable';
import uuid from '../uuid';
// Default settings are in action/settings.js
//
@ -15,22 +13,12 @@ const initialState = ImmutableMap({
saved: true,
});
const defaultColumns = fromJS([
{ id: 'COMPOSE', uuid: uuid(), params: {} },
{ id: 'HOME', uuid: uuid(), params: {} },
{ id: 'NOTIFICATIONS', uuid: uuid(), params: {} },
]);
const hydrate = (state, settings) => state.mergeDeep(settings).update('columns', (val = defaultColumns) => val);
const updateFrequentEmojis = (state, emoji) => state.update('frequentlyUsedEmojis', ImmutableMap(), map => map.update(emoji.id, 0, count => count + 1)).set('saved', false);
const filterDeadListColumns = (state, listId) => state.update('columns', columns => columns.filterNot(column => column.get('id') === 'LIST' && column.get('params').get('id') === listId));
export default function settings(state = initialState, action) {
switch(action.type) {
case STORE_HYDRATE:
return hydrate(state, action.state.get('settings'));
case ME_FETCH_SUCCESS:
const me = fromJS(action.me);
let fePrefs = me.getIn(['pleroma', 'settings_store', FE_NAME], ImmutableMap());

@ -2,6 +2,7 @@ import {
REBLOG_REQUEST,
REBLOG_FAIL,
FAVOURITE_REQUEST,
UNFAVOURITE_REQUEST,
FAVOURITE_FAIL,
} from '../actions/interactions';
import {
@ -12,11 +13,12 @@ import {
} from '../actions/statuses';
import {
EMOJI_REACT_REQUEST,
UNEMOJI_REACT_REQUEST,
} from '../actions/emoji_reacts';
import { TIMELINE_DELETE } from '../actions/timelines';
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
import { Map as ImmutableMap, fromJS } from 'immutable';
import { simulateEmojiReact } from 'soapbox/utils/emoji_reacts';
import { simulateEmojiReact, simulateUnEmojiReact } from 'soapbox/utils/emoji_reacts';
const importStatus = (state, status) => state.set(status.id, fromJS(status));
@ -40,11 +42,27 @@ export default function statuses(state = initialState, action) {
case STATUSES_IMPORT:
return importStatuses(state, action.statuses);
case FAVOURITE_REQUEST:
return state.setIn([action.status.get('id'), 'favourited'], true);
return state.update(action.status.get('id'), status =>
status
.set('favourited', true)
.update('favourites_count', count => count + 1));
case UNFAVOURITE_REQUEST:
return state.update(action.status.get('id'), status =>
status
.set('favourited', false)
.update('favourites_count', count => Math.max(0, count - 1)));
case EMOJI_REACT_REQUEST:
const path = [action.status.get('id'), 'pleroma', 'emoji_reactions'];
const emojiReacts = state.getIn(path);
return state.setIn(path, simulateEmojiReact(emojiReacts, action.emoji));
return state
.updateIn(
[action.status.get('id'), 'pleroma', 'emoji_reactions'],
emojiReacts => simulateEmojiReact(emojiReacts, action.emoji)
);
case UNEMOJI_REACT_REQUEST:
return state
.updateIn(
[action.status.get('id'), 'pleroma', 'emoji_reactions'],
emojiReacts => simulateUnEmojiReact(emojiReacts, action.emoji)
);
case FAVOURITE_FAIL:
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'favourited'], false);
case REBLOG_REQUEST:

@ -65,10 +65,10 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
}));
};
const updateTimeline = (state, timeline, status) => {
const updateTimeline = (state, timeline, statusId) => {
const top = state.getIn([timeline, 'top']);
const ids = state.getIn([timeline, 'items'], ImmutableList());
const includesId = ids.includes(status.get('id'));
const includesId = ids.includes(statusId);
const unread = state.getIn([timeline, 'unread'], 0);
if (includesId) {
@ -80,17 +80,17 @@ const updateTimeline = (state, timeline, status) => {
return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
if (!top) mMap.set('unread', unread + 1);
if (top && ids.size > 40) newIds = newIds.take(20);
mMap.set('items', newIds.unshift(status.get('id')));
mMap.set('items', newIds.unshift(statusId));
}));
};
const updateTimelineQueue = (state, timeline, status) => {
const updateTimelineQueue = (state, timeline, statusId) => {
const queuedStatuses = state.getIn([timeline, 'queuedItems'], ImmutableList());
const listedStatuses = state.getIn([timeline, 'items'], ImmutableList());
const totalQueuedItemsCount = state.getIn([timeline, 'totalQueuedItemsCount'], 0);
let alreadyExists = queuedStatuses.find(existingQueuedStatus => existingQueuedStatus.get('id') === status.get('id'));
if (!alreadyExists) alreadyExists = listedStatuses.find(existingListedStatusId => existingListedStatusId === status.get('id'));
let alreadyExists = queuedStatuses.find(existingQueuedStatus => existingQueuedStatus === statusId);
if (!alreadyExists) alreadyExists = listedStatuses.find(existingListedStatusId => existingListedStatusId === statusId);
if (alreadyExists) {
return state;
@ -100,7 +100,7 @@ const updateTimelineQueue = (state, timeline, status) => {
return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
if (totalQueuedItemsCount <= MAX_QUEUED_ITEMS) {
mMap.set('queuedItems', newQueuedStatuses.push(status));
mMap.set('queuedItems', newQueuedStatuses.push(statusId));
}
mMap.set('totalQueuedItemsCount', totalQueuedItemsCount + 1);
}));
@ -165,9 +165,9 @@ export default function timelines(state = initialState, action) {
case TIMELINE_EXPAND_SUCCESS:
return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent);
case TIMELINE_UPDATE:
return updateTimeline(state, action.timeline, fromJS(action.status));
return updateTimeline(state, action.timeline, action.statusId);
case TIMELINE_UPDATE_QUEUE:
return updateTimelineQueue(state, action.timeline, fromJS(action.status));
return updateTimelineQueue(state, action.timeline, action.statusId);
case TIMELINE_DEQUEUE:
return state.update(action.timeline, initialTimeline, map => map.withMutations(mMap => {
mMap.set('queuedItems', ImmutableList());

@ -6,6 +6,7 @@ import {
reduceEmoji,
getReactForStatus,
simulateEmojiReact,
simulateUnEmojiReact,
} from '../emoji_reacts';
import { fromJS } from 'immutable';
@ -205,3 +206,28 @@ describe('simulateEmojiReact', () => {
]));
});
});
describe('simulateUnEmojiReact', () => {
it('removes the emoji from the list', () => {
const emojiReacts = fromJS([
{ 'count': 2, 'me': false, 'name': '👍' },
{ 'count': 3, 'me': true, 'name': '❤' },
]);
expect(simulateUnEmojiReact(emojiReacts, '❤')).toEqual(fromJS([
{ 'count': 2, 'me': false, 'name': '👍' },
{ 'count': 2, 'me': false, 'name': '❤' },
]));
});
it('removes the emoji if it\'s the last one in the list', () => {
const emojiReacts = fromJS([
{ 'count': 2, 'me': false, 'name': '👍' },
{ 'count': 2, 'me': false, 'name': '❤' },
{ 'count': 1, 'me': true, 'name': '😯' },
]);
expect(simulateUnEmojiReact(emojiReacts, '😯')).toEqual(fromJS([
{ 'count': 2, 'me': false, 'name': '👍' },
{ 'count': 2, 'me': false, 'name': '❤' },
]));
});
});

@ -100,3 +100,20 @@ export const simulateEmojiReact = (emojiReacts, emoji) => {
}));
}
};
export const simulateUnEmojiReact = (emojiReacts, emoji) => {
const idx = emojiReacts.findIndex(e =>
e.get('name') === emoji && e.get('me') === true);
if (idx > -1) {
const emojiReact = emojiReacts.get(idx);
const newCount = emojiReact.get('count') - 1;
if (newCount < 1) return emojiReacts.delete(idx);
return emojiReacts.set(idx, emojiReact.merge({
count: emojiReact.get('count') - 1,
me: false,
}));
} else {
return emojiReacts;
}
};

@ -1,14 +1,14 @@
// Detect backend features to conditionally render elements
import semver from 'semver';
import gte from 'semver/functions/gte';
export const getFeatures = instance => {
const v = parseVersion(instance.get('version'));
return {
suggestions: v.software === 'Mastodon' && semver.gte(v.compatVersion, '2.4.3'),
trends: v.software === 'Mastodon' && semver.gte(v.compatVersion, '3.0.0'),
emojiReacts: v.software === 'Pleroma' && semver.gte(v.version, '2.0.0'),
suggestions: v.software === 'Mastodon' && gte(v.compatVersion, '2.4.3'),
trends: v.software === 'Mastodon' && gte(v.compatVersion, '3.0.0'),
emojiReacts: v.software === 'Pleroma' && gte(v.version, '2.0.0'),
attachmentLimit: v.software === 'Pleroma' ? Infinity : 4,
focalPoint: v.software === 'Mastodon' && semver.gte(v.compatVersion, '2.3.0'),
focalPoint: v.software === 'Mastodon' && gte(v.compatVersion, '2.3.0'),
};
};

@ -1,13 +1,72 @@
import { Map as ImmutableMap } from 'immutable';
import { convert } from 'chromatism';
export const generateThemeCss = brandColor => {
if (!brandColor) return null;
return themeDataToCss(brandColorToThemeData(brandColor));
};
// https://stackoverflow.com/a/5624139
function hexToRgb(hex) {
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, (m, r, g, b) => (
r + r + g + g + b + b
));
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
} : {
// fall back to Azure
r: 4,
g: 130,
b: 216,
};
}
// Taken from chromatism.js
// https://github.com/graypegg/chromatism/blob/master/src/conversions/rgb.js
const rgbToHsl = value => {
var r = value.r / 255;
var g = value.g / 255;
var b = value.b / 255;
var rgbOrdered = [ r, g, b ].sort();
var l = ((rgbOrdered[0] + rgbOrdered[2]) / 2) * 100;
var s, h;
if (rgbOrdered[0] === rgbOrdered[2]) {
s = 0;
h = 0;
} else {
if (l >= 50) {
s = ((rgbOrdered[2] - rgbOrdered[0]) / ((2.0 - rgbOrdered[2]) - rgbOrdered[0])) * 100;
} else {
s = ((rgbOrdered[2] - rgbOrdered[0]) / (rgbOrdered[2] + rgbOrdered[0])) * 100;
}
if (rgbOrdered[2] === r) {
h = ((g - b) / (rgbOrdered[2] - rgbOrdered[0])) * 60;
} else if (rgbOrdered[2] === g) {
h = (2 + ((b - r) / (rgbOrdered[2] - rgbOrdered[0]))) * 60;
} else {
h = (4 + ((r - g) / (rgbOrdered[2] - rgbOrdered[0]))) * 60;
}
if (h < 0) {
h += 360;
} else if (h > 360) {
h = h % 360;
}
}
return {
h: h,
s: s,
l: l,
};
};
export const brandColorToThemeData = brandColor => {
const { h, s, l } = convert(brandColor).hsl;
const { h, s, l } = rgbToHsl(hexToRgb(brandColor));
return ImmutableMap({
'brand-color_h': h,
'brand-color_s': `${s}%`,

File diff suppressed because it is too large Load Diff

@ -177,31 +177,6 @@
}
}
.nothing-here {
background: var(--brand-color--med);
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
color: var(--primary-text-color--faint);
font-size: 14px;
font-weight: 500;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
cursor: default;
border-radius: 4px;
padding: 20px;
min-height: 30vh;
&--under-tabs {
border-radius: 0 0 4px 4px;
}
&--flexible {
box-sizing: border-box;
min-height: 100%;
}
}
.account-role {
display: inline-block;
padding: 4px 6px;
@ -227,104 +202,6 @@
}
}
.account__header__fields {
padding: 0;
margin: 15px -15px -15px;
border: 0 none;
border-top: 1px solid var(--brand-color--med);
border-bottom: 1px solid var(--brand-color--med);
font-size: 14px;
line-height: 20px;
dl {
display: flex;
border-bottom: 1px solid var(--brand-color--med);
}
dt,
dd {
box-sizing: border-box;
padding: 14px;
text-align: center;
max-height: 48px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
dt {
font-weight: 500;
width: 120px;
flex: 0 0 auto;
color: var(--primary-text-color--faint);
background: hsla(var(--background-color_hsl), 0.5);
}
dd {
flex: 1 1 auto;
color: var(--primary-text-color--faint);
}
a {
color: var(--highlight-text-color);
text-decoration: none;
&:hover,
&:focus,
&:active {
text-decoration: underline;
}
}
.verified {
border: 1px solid rgba($valid-value-color, 0.5);
background: rgba($valid-value-color, 0.25);
a {
color: $valid-value-color;
font-weight: 500;
}
&__mark {
color: $valid-value-color;
}
}
dl:last-child {
border-bottom: 0;
}
}
.directory__tag .trends__item__current {
width: auto;
}
.pending-account {
&__header {
color: var(--primary-text-color--faint);
a {
color: var(--background-color);
text-decoration: none;
&:hover,
&:active,
&:focus {
text-decoration: underline;
}
}
strong {
color: var(--primary-text-color);
font-weight: 700;
}
}
&__body {
margin-top: 10px;
}
}
.account {
padding: 10px;
position: relative;

@ -5,22 +5,16 @@
@import 'reset';
@import 'basics';
@import 'containers';
@import 'lists';
@import 'footer';
@import 'compact_header';
@import 'widgets';
@import 'forms';
@import 'accounts';
@import 'stream_entries';
@import 'boost';
@import 'loading';
@import 'ui';
@import 'polls';
@import 'introduction';
// @import 'introduction';
@import 'emoji_picker';
@import 'about';
@import 'tables';
@import 'dashboard';
@import 'rtl';
@import 'accessibility';
@import 'donations';
@ -38,10 +32,10 @@
@import 'components/account-header';
@import 'components/user-panel';
@import 'components/compose-form';
@import 'components/group-card';
@import 'components/group-detail';
@import 'components/group-form';
@import 'components/group-sidebar-panel';
// @import 'components/group-card';
// @import 'components/group-detail';
// @import 'components/group-form';
// @import 'components/group-sidebar-panel';
@import 'components/sidebar-menu';
@import 'components/hotkeys-modal';
@import 'components/emoji-reacts';
@ -77,3 +71,6 @@
@import 'components/filters';
@import 'components/mfa_form';
@import 'components/snackbar';
// Holiday
@import 'holiday/halloween';

@ -176,13 +176,6 @@ body {
margin-top: 1em;
}
&__dismiss {
display: inline-block;
text-transform: uppercase;
margin-left: 5px;
font-size: 13px;
}
a {
color: var(--brand-color--hicontrast);
text-decoration: underline;

@ -5,7 +5,7 @@
bottom: 0;
right: 20px;
width: 265px;
height: 265px;
height: 350px;
max-height: calc(100vh - 70px);
display: flex;
flex-direction: column;
@ -146,14 +146,23 @@
max-width: 70%;
border-radius: 10px;
background-color: var(--background-color);
overflow: hidden;
text-overflow: ellipsis;
overflow-wrap: break-word;
white-space: break-spaces;
position: relative;
a {
color: var(--brand-color--hicontrast);
}
&:hover,
&:focus,
&:active, {
.chat-message__menu {
opacity: 1;
pointer-events: all;
}
}
}
&--me .chat-message__bubble {
@ -164,6 +173,17 @@
&--pending .chat-message__bubble {
opacity: 0.5;
}
&__menu {
position: absolute;
top: -8px;
right: -8px;
background: var(--background-color);
border-radius: 999px;
opacity: 0;
pointer-events: none;
transition: 0.2s;
}
}
.chat-list {

@ -1,34 +0,0 @@
.compact-header {
h1 {
font-size: 24px;
line-height: 28px;
color: var(--primary-text-color--faint);
font-weight: 500;
margin-bottom: 20px;
padding: 0 10px;
word-wrap: break-word;
@media screen and (max-width: 740px) {
text-align: center;
padding: 20px 10px 0;
}
a {
color: inherit;
text-decoration: none;
}
small {
font-weight: 400;
color: var(--primary-text-color--faint);
}
img {
display: inline-block;
margin-bottom: -5px;
margin-right: 15px;
width: 36px;
height: 36px;
}
}
}

@ -713,5 +713,6 @@
.react-toggle-track-check,
.react-toggle-track-x {
height: 16px;
color: white;
}
}

@ -114,6 +114,10 @@
color: var(--primary-text-color);
}
span.hover-ref-wrapper {
display: inline;
}
.display-name__account {
display: block;
margin-top: -10px;

@ -1,14 +1,3 @@
.container-alt {
width: 700px;
margin: 0 auto;
margin-top: 40px;
@media screen and (max-width: 740px) {
width: 100%;
margin: 0;
}
}
.logo-container {
margin: 100px auto 50px;
@ -111,80 +100,3 @@
margin-left: 8px;
}
}
.grid-3 {
display: grid;
grid-gap: 10px;
grid-template-columns: 3fr 1fr;
grid-auto-columns: 25%;
grid-auto-rows: max-content;
.column-0 {
grid-column: 1 / 3;
grid-row: 1;
}
.column-1 {
grid-column: 1;
grid-row: 2;
}
.column-2 {
grid-column: 2;
grid-row: 2;
}
.column-3 {
grid-column: 1 / 3;
grid-row: 3;
}
.landing-page__call-to-action {
min-height: 100%;
}
@media screen and (max-width: 738px) {
grid-template-columns: minmax(0, 50%) minmax(0, 50%);
.landing-page__call-to-action {
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.row__information-board {
width: 100%;
justify-content: center;
align-items: center;
}
.row__mascot {
display: none;
}
}
@media screen and (max-width: $no-gap-breakpoint) {
grid-gap: 0;
grid-template-columns: minmax(0, 100%);
.column-0 {
grid-column: 1;
}
.column-1 {
grid-column: 1;
grid-row: 3;
}
.column-2 {
grid-column: 1;
grid-row: 2;
}
.column-3 {
grid-column: 1;
grid-row: 4;
}
}
}

@ -1,76 +0,0 @@
.dashboard__counters {
display: flex;
flex-wrap: wrap;
margin: 0 -5px;
margin-bottom: 20px;
& > div {
box-sizing: border-box;
flex: 0 0 33.333%;
padding: 0 5px;
margin-bottom: 10px;
& > div,
& > a {
padding: 20px;
background: var(--brand-color--faint);
border-radius: 4px;
}
& > a {
text-decoration: none;
color: inherit;
display: block;
&:hover,
&:focus,
&:active {
background: var(--brand-color--med);
}
}
}
&__num,
&__text {
text-align: center;
font-weight: 500;
font-size: 24px;
line-height: 21px;
color: var(--primary-text-color);
font-family: var(--font-display), sans-serif;
margin-bottom: 20px;
line-height: 30px;
}
&__text {
font-size: 18px;
}
&__label {
font-size: 14px;
color: var(--primary-text-color--faint);
text-align: center;
font-weight: 500;
}
}
.dashboard__widgets {
display: flex;
flex-wrap: wrap;
margin: 0 -5px;
& > div {
flex: 0 0 33.333%;
margin-bottom: 20px;
& > div {
padding: 0 5px;
}
}
a:not(.name-tag) {
color: var(--background-color);
font-weight: 500;
text-decoration: none;
}
}

@ -1,5 +1,6 @@
.dyslexic {
font-family: 'OpenDyslexic' !important;
margin-bottom: 8px;
}
body.dyslexic {

@ -13,7 +13,7 @@ code {
.simple_form {
.input {
margin-bottom: 15px;
margin-bottom: 8px;
overflow: hidden;
&.hidden {
@ -43,7 +43,6 @@ code {
&.boolean {
position: relative;
margin-bottom: 0;
.label_input > label {
font-family: inherit;
@ -111,7 +110,6 @@ code {
span.hint {
display: block;
font-size: 12px;
margin-top: 4px;
}
p.hint {
@ -172,15 +170,10 @@ code {
font-size: 14px;
color: var(--primary-text-color);
display: block;
margin-bottom: 8px;
word-wrap: break-word;
font-weight: 500;
}
.hint {
margin-top: 6px;
}
ul {
flex: 390px;
}
@ -436,6 +429,7 @@ code {
}
select {
appearance: none;
box-sizing: border-box;
font-size: 16px;
color: var(--primary-text-color);
@ -451,6 +445,23 @@ code {
padding-right: 30px;
height: 41px;
position: relative;
margin-top: 8px;
cursor: pointer;
}
.select-wrapper::after {
display: block;
font-family: 'ForkAwesome';
content: '';
width: 10px;
position: absolute;
right: 12px;
top: 1px;
border-left: 1px solid var(--highlight-text-color);
height: 39px;
padding: 12px;
box-sizing: border-box;
pointer-events: none;
}
.label_input {
@ -542,155 +553,6 @@ code {
font-size: 24px;
}
.flash-message {
background: var(--brand-color--med);
color: var(--primary-text-color--faint);
border-radius: 4px;
padding: 15px 10px;
margin-bottom: 30px;
text-align: center;
&.notice {
border: 1px solid rgba($valid-value-color, 0.5);
background: rgba($valid-value-color, 0.25);
color: $valid-value-color;
}
&.alert {
border: 1px solid rgba($error-value-color, 0.5);
background: rgba($error-value-color, 0.25);
color: $error-value-color;
}
a {
display: inline-block;
color: var(--primary-text-color--faint);
text-decoration: none;
&:hover {
color: var(--primary-text-color);
text-decoration: underline;
}
}
p {
margin-bottom: 15px;
}
.oauth-code {
outline: 0;
box-sizing: border-box;
display: block;
width: 100%;
border: 0;
padding: 10px;
font-family: var(--font-monospace), monospace;
background: var(--brand-color--med);
color: var(--primary-text-color);
font-size: 14px;
margin: 0;
&::-moz-focus-inner {
border: 0;
}
&::-moz-focus-inner,
&:focus,
&:active {
outline: 0 !important;
}
&:focus {
background: var(--brand-color--faint);
}
}
strong {
font-weight: 500;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
@media screen and (max-width: 740px) and (min-width: 441px) {
margin-top: 40px;
}
}
.form-footer {
margin-top: 30px;
text-align: center;
a {
color: var(--primary-text-color--faint);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
.quick-nav {
list-style: none;
margin-bottom: 25px;
font-size: 14px;
li {
display: inline-block;
margin-right: 10px;
}
a {
color: var(--highlight-text-color);
text-transform: uppercase;
text-decoration: none;
font-weight: 700;
&:hover,
&:focus,
&:active {
color: var(--highlight-text-color);
}
}
}
.oauth-prompt,
.follow-prompt {
margin-bottom: 30px;
color: var(--primary-text-color--faint);
h2 {
font-size: 16px;
margin-bottom: 30px;
text-align: center;
}
strong {
color: var(--primary-text-color--faint);
font-weight: 500;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
@media screen and (max-width: 740px) and (min-width: 441px) {
margin-top: 40px;
}
}
.qr-wrapper {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
}
.qr-code {
flex: 0 0 auto;
background: var(--foreground-color);
@ -705,35 +567,7 @@ code {
}
}
.qr-alternative {
margin-bottom: 20px;
color: var(--primary-text-color--faint);
flex: 150px;
samp {
display: block;
font-size: 14px;
}
}
.table-form {
p {
margin-bottom: 15px;
strong {
font-weight: 500;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
}
}
.simple_form,
.table-form {
.simple_form {
.warning {
box-sizing: border-box;
background: rgba($error-value-color, 0.5);
@ -773,173 +607,6 @@ code {
}
}
.action-pagination {
display: flex;
flex-wrap: wrap;
align-items: center;
.actions,
.pagination {
flex: 1 1 auto;
}
.actions {
padding: 30px 0;
padding-right: 20px;
flex: 0 0 auto;
}
}
.post-follow-actions {
text-align: center;
color: var(--primary-text-color--faint);
div {
margin-bottom: 4px;
}
}
.alternative-login {
margin-top: 20px;
margin-bottom: 20px;
h4 {
font-size: 16px;
color: var(--primary-text-color);
text-align: center;
margin-bottom: 20px;
border: 0;
padding: 0;
}
.button {
display: block;
}
}
.scope-danger {
color: $warning-red;
}
.form_admin_settings_site_short_description,
.form_admin_settings_site_description,
.form_admin_settings_site_extended_description,
.form_admin_settings_site_terms,
.form_admin_settings_custom_css,
.form_admin_settings_closed_registrations_message {
textarea {
font-family: var(--font-monospace), monospace;
}
}
.input-copy {
background: var(--background-color);
border: 1px solid var(--background-color);
border-radius: 4px;
display: flex;
align-items: center;
padding-right: 4px;
position: relative;
top: 1px;
transition: border-color 300ms linear;
&__wrapper {
flex: 1 1 auto;
}
input[type=text] {
background: transparent;
border: 0;
padding: 10px;
font-size: 14px;
font-family: var(--font-monospace), monospace;
}
button {
flex: 0 0 auto;
margin: 4px;
text-transform: none;
font-weight: 400;
font-size: 14px;
padding: 7px 18px;
padding-bottom: 6px;
width: auto;
transition: background 300ms linear;
}
&.copied {
border-color: $valid-value-color;
transition: none;
button {
background: $valid-value-color;
transition: none;
}
}
}
.connection-prompt {
margin-bottom: 25px;
.fa-link {
background-color: var(--brand-color--med);
border-radius: 100%;
font-size: 24px;
padding: 10px;
}
&__column {
align-items: center;
display: flex;
flex: 1;
flex-direction: column;
flex-shrink: 1;
max-width: 50%;
&-sep {
align-self: center;
flex-grow: 0;
overflow: visible;
position: relative;
z-index: 1;
}
p {
word-break: break-word;
}
}
.account__avatar {
margin-bottom: 20px;
}
&__connection {
background-color: var(--brand-color--med);
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
border-radius: 4px;
padding: 25px 10px;
position: relative;
text-align: center;
&::after {
background-color: var(--brand-color--med);
content: '';
display: block;
height: 100%;
left: 50%;
position: absolute;
top: 0;
width: 1px;
}
}
&__row {
align-items: flex-start;
display: flex;
flex-direction: row;
}
}
.columns-area {
form.simple_form {
padding: 15px;

@ -0,0 +1,158 @@
body.halloween {
// Set brand color to orange
--brand-color_h: 29.727272727272727;
--brand-color_s: 100%;
--brand-color_l: 43.13725490196079%;
// Stars BG
background-color: #904700; // Color matches twinkle.svg
background-image: url('../images/halloween/starfield.png');
background-size: cover;
background-attachment: fixed;
background-position: center;
// Full-screen pseudo-elements to hold BG graphics
&::before,
&::after,
.app-holder::before,
.app-holder::after {
content: '';
display: block;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-size: cover;
background-position: center;
width: 100%;
height: 100%;
z-index: -100;
}
// Spiderweb BG
&::before {
background-image: url('../images/halloween/spiderweb.svg');
}
// Twinkle effect by masking with semi-transparent animated circles
&::after {
z-index: -101;
background: transparent url("../images/halloween/twinkle.svg") repeat top center;
animation: halloween-twinkle 200s linear infinite;
}
.app-holder {
// Vignette
&::before {
background-image: radial-gradient(
circle,
transparent 0%,
transparent 60%,
var(--vignette-color) 100%
);
}
// Floating clouds BG
&::after {
background: transparent url("../images/halloween/clouds.png") repeat top center;
animation: halloween-clouds 200s linear infinite;
}
}
// Dangling spider
.ui .page__top::after,
.ui .page__columns::after {
content: '';
display: block;
width: 100px;
height: 100px;
right: 20px;
background-image: url('../images/halloween/spider.svg');
background-size: contain;
background-repeat: no-repeat;
background-position: top right;
z-index: -1;
pointer-events: none;
}
.ui .page__columns::after {
position: fixed;
top: 50px;
}
.ui .page__top::after {
position: absolute;
bottom: -100px;
}
.ui .page__top + .page__columns::after {
display: none;
}
// Witch emblem
.getting-started__footer::before {
content: '';
display: block;
background-image: url('../images/halloween/halloween-emblem.svg');
background-size: contain;
background-position: left;
background-repeat: no-repeat;
width: 100%;
height: 100px;
margin-bottom: 20px;
}
// Color fixes
// Elements directly over the BG need static colors that don't change
// regardless of the theme-mode
.getting-started__footer {
color: #fff;
a {
color: hsla(0, 0%, 100%, 0.4);
}
p {
color: hsla(0, 0%, 100%, 0.8);
}
}
.profile-info-panel {
color: #fff;
&-content__name h1 {
span:first-of-type {
color: hsla(0, 0%, 100%, 0.6);
}
small {
color: #fff;
}
}
&-content__bio {
color: #fff;
}
&-content__bio a,
&-content__fields a {
color: hsl(
var(--brand-color_h),
var(--brand-color_s),
calc(var(--brand-color_l) + 8%)
);
}
}
}
// Animations
@keyframes halloween-twinkle {
from { background-position: 0 0; }
to { background-position: -10000px 5000px; }
}
@keyframes halloween-clouds {
from { background-position: 0 0; }
to { background-position: 10000px 0; }
}

@ -1,19 +0,0 @@
.no-list {
list-style: none;
li {
display: inline-block;
margin: 0 5px;
}
}
.recovery-codes {
list-style: none;
margin: 0 auto;
li {
font-size: 125%;
line-height: 1.5;
letter-spacing: 1px;
}
}

@ -7,16 +7,6 @@ body.rtl {
padding-right: 15px;
}
.landing-page__logo {
margin-right: 0;
margin-left: 20px;
}
.landing-page .features-list .features-list__row .visual {
margin-left: 0;
margin-right: 15px;
}
.column-link__icon,
.column-header__icon {
margin-right: 0;
@ -83,23 +73,16 @@ body.rtl {
right: 10px;
}
.status,
.activity-stream .status.light {
.status {
padding-left: 10px;
padding-right: 68px;
}
.status__info .status__display-name,
.activity-stream .status.light .status__display-name {
.status__info .status__display-name {
padding-left: 25px;
padding-right: 0;
}
.activity-stream .pre-header {
padding-right: 68px;
padding-left: 0;
}
.status__prepend {
margin-left: 0;
margin-right: 68px;
@ -110,11 +93,6 @@ body.rtl {
right: -26px;
}
.activity-stream .pre-header .pre-header__icon {
left: auto;
right: 42px;
}
.account__avatar-overlay-overlay {
right: auto;
left: 0;
@ -125,8 +103,7 @@ body.rtl {
left: 0;
}
.status__relative-time,
.activity-stream .status.light .status__header .status__meta {
.status__relative-time {
float: left;
}
@ -256,44 +233,6 @@ body.rtl {
margin-left: 45px;
}
.landing-page .header-wrapper .mascot {
right: 60px;
left: auto;
}
.landing-page__call-to-action .row__information-board {
direction: rtl;
}
.landing-page .header .hero .floats .float-1 {
left: -120px;
right: auto;
}
.landing-page .header .hero .floats .float-2 {
left: 210px;
right: auto;
}
.landing-page .header .hero .floats .float-3 {
left: 110px;
right: auto;
}
.landing-page .header .links .brand img {
left: 0;
}
.landing-page .fa-external-link {
padding-right: 5px;
padding-left: 0 !important;
}
.landing-page .features #soapbox-timeline {
margin-right: 0;
margin-left: 30px;
}
@media screen and (min-width: 631px) {
.column,
.drawer {
@ -329,18 +268,6 @@ body.rtl {
}
}
.landing-page__information {
.account__display-name {
margin-right: 0;
margin-left: 5px;
}
.account__avatar-wrapper {
margin-left: 12px;
margin-right: 0;
}
}
.card__bar .display-name {
margin-left: 0;
margin-right: 15px;

@ -1,159 +0,0 @@
.activity-stream {
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
border-radius: 4px;
overflow: hidden;
margin-bottom: 10px;
@media screen and (max-width: $no-gap-breakpoint) {
margin-bottom: 0;
border-radius: 0;
box-shadow: none;
}
&--headless {
border-radius: 0;
margin: 0;
box-shadow: none;
.detailed-status,
.status {
border-radius: 0 !important;
}
}
div[data-component] {
width: 100%;
}
.entry {
background: var(--brand-color--med);
.detailed-status,
.status,
.load-more {
animation: none;
}
&:last-child {
.detailed-status,
.status,
.load-more {
border-bottom: 0;
border-radius: 0 0 4px 4px;
}
}
&:first-child {
.detailed-status,
.status,
.load-more {
border-radius: 4px 4px 0 0;
}
&:last-child {
.detailed-status,
.status,
.load-more {
border-radius: 4px;
}
}
}
@media screen and (max-width: 740px) {
.detailed-status,
.status,
.load-more {
border-radius: 0 !important;
}
}
}
&--highlighted .entry {
background: var(--brand-color--med);
}
}
.button.logo-button {
flex: 0 auto;
font-size: 14px;
background: var(--brand-color);
color: #fff;
text-transform: none;
line-height: 36px;
height: auto;
padding: 3px 15px;
border: 0;
svg {
width: 20px;
height: auto;
vertical-align: middle;
margin-right: 5px;
fill: var(--primary-text-color);
}
&:active,
&:focus,
&:hover {
background: var(--brand-color--hicontrast);
}
&:disabled,
&.disabled {
&:active,
&:focus,
&:hover {
background: var(--brand-color--med);
}
}
&.button--destructive {
&:active,
&:focus,
&:hover {
background: $error-red;
}
}
@media screen and (max-width: $no-gap-breakpoint) {
svg {
display: none;
}
}
}
.embed,
.public-layout {
.detailed-status {
padding: 15px;
}
.status {
padding: 15px 15px 15px (48px + 15px * 2);
min-height: 48px + 2px;
&__avatar {
left: 15px;
top: 17px;
}
&__content {
padding-top: 5px;
}
&__prepend {
margin-left: 48px + 15px * 2;
padding-top: 15px;
}
&__prepend-icon-wrapper {
left: -32px;
}
.media-gallery,
&__action-bar,
.video-player {
margin-top: 10px;
}
}
}

@ -1,243 +0,0 @@
.table {
width: 100%;
max-width: 100%;
border-spacing: 0;
border-collapse: collapse;
th,
td {
padding: 8px;
line-height: 18px;
vertical-align: top;
border-top: 1px solid var(--brand-color--med);
text-align: left;
background: var(--brand-color--med);
}
& > thead > tr > th {
vertical-align: bottom;
border-bottom: 2px solid var(--brand-color--med);
border-top: 0;
font-weight: 500;
}
& > tbody > tr > th {
font-weight: 500;
}
& > tbody > tr:nth-child(odd) > td,
& > tbody > tr:nth-child(odd) > th {
background: var(--brand-color--med);
}
a {
color: var(--highlight-text-color);
text-decoration: underline;
&:hover {
text-decoration: none;
}
}
strong {
font-weight: 500;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
&.inline-table {
& > tbody > tr:nth-child(odd) {
& > td,
& > th {
background: transparent;
}
}
& > tbody > tr:first-child {
& > td,
& > th {
border-top: 0;
}
}
}
&.batch-table {
& > thead > tr > th {
background: var(--brand-color--med);
border-top: 1px solid var(--background-color);
border-bottom: 1px solid var(--background-color);
&:first-child {
border-radius: 4px 0 0;
border-left: 1px solid var(--background-color);
}
&:last-child {
border-radius: 0 4px 0 0;
border-right: 1px solid var(--background-color);
}
}
}
&--invites tbody td {
vertical-align: middle;
}
}
.table-wrapper {
overflow: auto;
margin-bottom: 20px;
}
samp {
font-family: var(--font-monospace), monospace;
}
button.table-action-link {
background: transparent;
border: 0;
font: inherit;
}
button.table-action-link,
a.table-action-link {
text-decoration: none;
display: inline-block;
margin-right: 5px;
padding: 0 10px;
color: var(--primary-text-color--faint);
font-weight: 500;
&:hover {
color: var(--primary-text-color);
}
i.fa {
font-weight: 400;
margin-right: 5px;
}
&:first-child {
padding-left: 0;
}
}
.batch-table {
&__toolbar,
&__row {
display: flex;
&__select {
box-sizing: border-box;
padding: 8px 16px;
cursor: pointer;
min-height: 100%;
input {
margin-top: 8px;
}
&--aligned {
display: flex;
align-items: center;
input {
margin-top: 0;
}
}
@media screen and (max-width: $no-gap-breakpoint) {
display: none;
}
}
&__actions,
&__content {
padding: 8px 0;
padding-right: 16px;
flex: 1 1 auto;
}
}
&__toolbar {
border: 1px solid var(--background-color);
background: var(--brand-color--med);
border-radius: 4px 0 0;
height: 47px;
align-items: center;
&__actions {
text-align: right;
padding-right: 16px - 5px;
}
@media screen and (max-width: $no-gap-breakpoint) {
display: none;
}
}
&__row {
border: 1px solid var(--background-color);
border-top: 0;
background: var(--brand-color--med);
@media screen and (max-width: $no-gap-breakpoint) {
&:first-child {
border-top: 1px solid var(--background-color);
}
}
&:hover {
background: var(--background-color);
}
&:nth-child(even) {
background: var(--brand-color--med);
&:hover {
background: var(--brand-color--faint);
}
}
&__content {
padding-top: 12px;
padding-bottom: 16px;
&--unpadded {
padding: 0;
}
}
}
.status__content {
padding-top: 0;
summary {
display: list-item;
}
strong {
font-weight: 700;
}
}
.nothing-here {
border: 1px solid var(--background-color);
border-top: 0;
box-shadow: none;
@media screen and (max-width: $no-gap-breakpoint) {
border-top: 1px solid var(--background-color);
}
}
@media screen and (max-width: 870px) {
.accounts-table tbody td.optional {
display: none;
}
}
}

@ -64,6 +64,7 @@ body.theme-mode-light {
var(--brand-color_s),
calc(var(--brand-color_l) - 8%)
);
--vignette-color: transparent;
// Meta-variables
--primary-text-color_h: 0;
@ -92,6 +93,7 @@ body.theme-mode-dark {
var(--brand-color_s),
calc(var(--brand-color_l) + 8%)
);
--vignette-color: #000;
// Meta-variables
--primary-text-color_h: 0;

@ -219,12 +219,6 @@
display: flex;
}
.domain_buttons {
height: 18px;
padding: 10px;
white-space: nowrap;
}
.muted {
.status__content p,
.status__content a {
@ -670,52 +664,6 @@
100% { opacity: 1; }
}
.layout-toggle {
display: flex;
padding: 5px;
button {
box-sizing: border-box;
flex: 0 0 50%;
background: transparent;
padding: 5px;
border: 0;
position: relative;
&:hover,
&:focus,
&:active {
svg path:first-child {
fill: var(--background-color);
}
}
}
svg {
width: 100%;
height: auto;
path:first-child {
fill: var(--brand-color--med);
}
path:last-child {
fill: var(--background-color);
}
}
&__active {
color: var(--brand-color);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--brand-color--med);
border-radius: 50%;
padding: 0.35rem;
}
}
.verified-icon {
display: inline-block;
margin: 0 4px 0 1px;

@ -1,534 +0,0 @@
.hero-widget {
margin-bottom: 10px;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
&__img {
width: 100%;
position: relative;
overflow: hidden;
border-radius: 4px 4px 0 0;
background: $base-shadow-color;
img {
object-fit: cover;
display: block;
width: 100%;
height: 100%;
margin: 0;
border-radius: 4px 4px 0 0;
}
}
&__text {
background: var(--brand-color--med);
padding: 20px;
border-radius: 0 0 4px 4px;
font-size: 15px;
color: var(--primary-text-color--faint);
line-height: 20px;
word-wrap: break-word;
font-weight: 400;
.emojione {
width: 20px;
height: 20px;
margin: -3px 0 0;
}
p {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
em {
display: inline;
margin: 0;
padding: 0;
font-weight: 700;
background: transparent;
font-family: inherit;
font-size: inherit;
line-height: inherit;
color: var(--primary-text-color);
}
a {
color: var(--primary-text-color--faint);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
@media screen and (max-width: $no-gap-breakpoint) {
display: none;
}
}
.endorsements-widget {
margin-bottom: 10px;
padding-bottom: 10px;
h4 {
padding: 10px;
text-transform: uppercase;
font-weight: 700;
font-size: 13px;
color: var(--primary-text-color--faint);
}
.account {
padding: 10px 0;
&:last-child {
border-bottom: 0;
}
.account__display-name {
display: flex;
align-items: center;
}
.account__avatar {
width: 44px;
height: 44px;
background-size: 44px 44px;
}
}
}
.box-widget {
padding: 20px;
border-radius: 4px;
background: var(--background-color);
box-shadow: 0 0 1px 1px rgba($base-shadow-color, 0.2);
}
.contact-widget,
.landing-page__information.contact-widget {
box-sizing: border-box;
padding: 20px;
min-height: 100%;
border-radius: 4px;
background: var(--brand-color--med);
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
}
.contact-widget {
font-size: 15px;
color: var(--primary-text-color--faint);
line-height: 20px;
word-wrap: break-word;
font-weight: 400;
strong {
font-weight: 500;
}
p {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
&__mail {
margin-top: 10px;
a {
color: var(--primary-text-color);
text-decoration: none;
}
}
}
.moved-account-widget {
padding: 15px;
padding-bottom: 20px;
border-radius: 4px;
background: var(--brand-color--med);
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
color: var(--primary-text-color--faint);
font-weight: 400;
margin-bottom: 10px;
strong,
a {
font-weight: 500;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
a {
color: inherit;
text-decoration: underline;
&.mention {
text-decoration: none;
span {
text-decoration: none;
}
&:focus,
&:hover,
&:active {
text-decoration: none;
span {
text-decoration: underline;
}
}
}
}
&__message {
margin-bottom: 15px;
.fa {
margin-right: 5px;
color: var(--primary-text-color--faint);
}
}
&__card {
.detailed-status__display-avatar {
position: relative;
cursor: pointer;
}
.detailed-status__display-name {
margin-bottom: 0;
text-decoration: none;
span {
font-weight: 400;
}
}
}
}
.memoriam-widget {
padding: 20px;
border-radius: 4px;
background: $base-shadow-color;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
font-size: 14px;
color: var(--primary-text-color--faint);
margin-bottom: 10px;
}
.page-header {
background: var(--brand-color--med);
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
border-radius: 4px;
padding: 60px 15px;
text-align: center;
margin: 10px 0;
h1 {
color: var(--primary-text-color);
font-size: 36px;
line-height: 1.1;
font-weight: 700;
margin-bottom: 10px;
}
p {
font-size: 15px;
color: var(--primary-text-color--faint);
}
@media screen and (max-width: $no-gap-breakpoint) {
margin-top: 0;
background: var(--brand-color--faint);
h1 {
font-size: 24px;
}
}
}
.directory {
background: var(--brand-color--med);
border-radius: 4px;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
&__tag {
box-sizing: border-box;
margin-bottom: 10px;
& > a,
& > div {
display: flex;
align-items: center;
justify-content: space-between;
background: var(--brand-color--med);
border-radius: 4px;
padding: 15px;
text-decoration: none;
color: inherit;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
}
& > a {
&:hover,
&:active,
&:focus {
background: var(--brand-color--med);
}
}
&.active > a {
background: var(--brand-color);
cursor: default;
}
&.disabled > div {
opacity: 0.5;
cursor: default;
}
h4 {
flex: 1 1 auto;
font-size: 18px;
font-weight: 700;
color: var(--primary-text-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.fa {
color: var(--primary-text-color--faint);
}
small {
display: block;
font-weight: 400;
font-size: 15px;
margin-top: 8px;
color: var(--primary-text-color--faint);
}
}
&.active h4 {
&,
.fa,
small {
color: var(--primary-text-color);
}
}
.avatar-stack {
flex: 0 0 auto;
width: (36px + 4px) * 3;
}
&.active .avatar-stack .account__avatar {
border-color: var(--brand-color);
}
}
}
.avatar-stack {
display: flex;
justify-content: flex-end;
.account__avatar {
flex: 0 0 auto;
width: 36px;
height: 36px;
border-radius: 50%;
position: relative;
margin-left: -10px;
background: var(--background-color);
border: 2px solid var(--brand-color--med);
&:nth-child(1) {
z-index: 1;
}
&:nth-child(2) {
z-index: 2;
}
&:nth-child(3) {
z-index: 3;
}
}
}
.accounts-table {
width: 100%;
.account {
padding: 0;
border: 0;
}
strong {
font-weight: 700;
}
thead th {
text-align: center;
text-transform: uppercase;
color: var(--primary-text-color--faint);
font-weight: 700;
padding: 10px;
&:first-child {
text-align: left;
}
}
tbody td {
padding: 15px 0;
vertical-align: middle;
border-bottom: 1px solid var(--brand-color--med);
}
tbody tr:last-child td {
border-bottom: 0;
}
&__count {
width: 120px;
text-align: center;
font-size: 15px;
font-weight: 500;
color: var(--primary-text-color);
small {
display: block;
color: var(--primary-text-color--faint);
font-weight: 400;
font-size: 14px;
}
}
&__comment {
width: 50%;
vertical-align: initial !important;
}
@media screen and (max-width: $no-gap-breakpoint) {
tbody td.optional {
display: none;
}
}
}
.moved-account-widget,
.memoriam-widget,
.box-widget,
.contact-widget,
.landing-page__information.contact-widget,
.directory,
.page-header {
@media screen and (max-width: $no-gap-breakpoint) {
margin-bottom: 0;
box-shadow: none;
border-radius: 0;
}
}
$maximum-width: 1235px;
$fluid-breakpoint: $maximum-width + 20px;
.statuses-grid {
min-height: 600px;
@media screen and (max-width: 640px) {
width: 100% !important; // Masonry layout is unnecessary at this width
}
&__item {
width: (960px - 20px) / 3;
@media screen and (max-width: $fluid-breakpoint) {
width: (940px - 20px) / 3;
}
@media screen and (max-width: 640px) {
width: 100%;
}
@media screen and (max-width: $no-gap-breakpoint) {
width: 100vw;
}
}
.detailed-status {
border-radius: 4px;
@media screen and (max-width: $no-gap-breakpoint) {
border-top: 1px solid var(--background-color);
}
&.compact {
.detailed-status__meta {
margin-top: 15px;
}
.status__content {
font-size: 15px;
line-height: 20px;
.emojione {
width: 20px;
height: 20px;
margin: -3px 0 0;
}
.status__content__spoiler-link {
line-height: 20px;
margin: 0;
}
}
.media-gallery,
.status-card,
.video-player {
margin-top: 15px;
}
}
}
}
.notice-widget {
margin-bottom: 10px;
color: var(--primary-text-color--faint);
p {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
a {
font-size: 14px;
line-height: 20px;
text-decoration: none;
font-weight: 500;
color: var(--brand-color);
&:hover,
&:focus,
&:active {
text-decoration: underline;
}
}
}

@ -0,0 +1,25 @@
# Installing Soapbox FE via YunoHost
If you want to install Soapbox FE to a Pleroma instance installed using [YunoHost](https://yunohost.org), you can do so by following these steps.
## 1. Download the build
First, download the latest build of Soapbox FE from GitLab.
```sh
curl -L https://gitlab.com/soapbox-pub/soapbox-fe/-/jobs/artifacts/v1.0.0/download?job=build-production -o soapbox-fe.zip
```
## 2. Unzip the build
Then, unzip the build to the Pleroma directory under YunoHost's directory:
```sh
busybox unzip soapbox-fe.zip -o -d /home/yunohost.app/pleroma/
```
**That's it! 🎉 Soapbox FE is installed.** The change will take effect immediately, just refresh your browser tab. It's not necessary to restart the Pleroma service.
---
Thank you to [@jeroen@social.franssen.xyz](https://social.franssen.xyz/@jeroen) for discovering this method.

@ -57,7 +57,6 @@
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"babel-runtime": "^6.26.0",
"blurhash": "^1.0.0",
"chromatism": "^3.0.0",
"classnames": "^2.2.5",
"compression-webpack-plugin": "^3.0.0",
"cross-env": "^6.0.0",
@ -102,7 +101,6 @@
"prop-types": "^15.5.10",
"punycode": "^2.1.0",
"qrcode.react": "^1.0.0",
"rails-ujs": "^5.2.3",
"react": "^16.13.1",
"react-color": "^2.18.1",
"react-dom": "^16.13.1",

@ -0,0 +1,5 @@
{
"extends": [
"config:base"
]
}

@ -3072,11 +3072,6 @@ chownr@^1.1.1:
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494"
integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==
chromatism@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/chromatism/-/chromatism-3.0.0.tgz#a7249d353c1e4f3577e444ac41171c4e2e624b12"
integrity sha1-pySdNTweTzV35ESsQRccTi5iSxI=
chrome-trace-event@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4"
@ -9406,11 +9401,6 @@ railroad-diagrams@^1.0.0:
resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=
rails-ujs@^5.2.3:
version "5.2.3"
resolved "https://registry.yarnpkg.com/rails-ujs/-/rails-ujs-5.2.3.tgz#4b65ea781a6befe62e96da6362165286a1fe4099"
integrity sha512-rYgj185MowWFBJI1wdac2FkX4yFYe4+3jJPlB+CTY7a4rmIyg0TqE4vYZmSBBesp7blPUa57oqKzwQjN7eVbEQ==
randexp@0.4.6:
version "0.4.6"
resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3"

Loading…
Cancel
Save