M app/javascript/flavours/glitch/actions/alerts.js => app/javascript/flavours/glitch/actions/alerts.js +17 -16
@@ 25,12 25,10 @@ export function clearAlert() {
};
}
-export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage, message_values = undefined) {
+export function showAlert(alert) {
return {
type: ALERT_SHOW,
- title,
- message,
- message_values,
+ alert,
};
}
@@ 44,20 42,23 @@ export function showAlertForError(error, skipNotFound = false) {
}
if (status === 429 && headers["x-ratelimit-reset"]) {
- const reset_date = new Date(headers["x-ratelimit-reset"]);
- return showAlert(messages.rateLimitedTitle, messages.rateLimitedMessage, { "retry_time": reset_date });
+ return showAlert({
+ title: messages.rateLimitedTitle,
+ message: messages.rateLimitedMessage,
+ values: { "retry_time": new Date(headers["x-ratelimit-reset"]) },
+ });
}
- let message = statusText;
- let title = `${status}`;
+ return showAlert({
+ title: `${status}`,
+ message: data.error || statusText,
+ });
+ }
- if (data.error) {
- message = data.error;
- }
+ console.error(error);
- return showAlert(title, message);
- } else {
- console.error(error);
- return showAlert();
- }
+ return showAlert({
+ title: messages.unexpectedTitle,
+ message: messages.unexpectedMessage,
+ });
}
A app/javascript/flavours/glitch/actions/bookmark_folders.js => app/javascript/flavours/glitch/actions/bookmark_folders.js +146 -0
@@ 0,0 1,146 @@
+import api from '../api';
+
+export const BOOKMARK_FOLDERS_FETCH_REQUEST = 'BOOKMARK_FOLDERS_FETCH_REQUEST';
+export const BOOKMARK_FOLDERS_FETCH_SUCCESS = 'BOOKMARK_FOLDERS_FETCH_SUCCESS';
+export const BOOKMARK_FOLDERS_FETCH_FAIL = 'BOOKMARK_FOLDERS_FETCH_FAIL';
+
+export const BOOKMARK_FOLDER_CREATE_REQUEST = 'BOOKMARK_FOLDER_CREATE_REQUEST';
+export const BOOKMARK_FOLDER_CREATE_SUCCESS = 'BOOKMARK_FOLDER_CREATE_SUCCESS';
+export const BOOKMARK_FOLDER_CREATE_FAIL = 'BOOKMARK_FOLDER_CREATE_FAIL';
+
+export const BOOKMARK_FOLDER_UPDATE_REQUEST = 'BOOKMARK_FOLDER_UPDATE_REQUEST';
+export const BOOKMARK_FOLDER_UPDATE_SUCCESS = 'BOOKMARK_FOLDER_UPDATE_SUCCESS';
+export const BOOKMARK_FOLDER_UPDATE_FAIL = 'BOOKMARK_FOLDER_UPDATE_FAIL';
+
+export const BOOKMARK_FOLDER_DELETE_REQUEST = 'BOOKMARK_FOLDER_DELETE_REQUEST';
+export const BOOKMARK_FOLDER_DELETE_SUCCESS = 'BOOKMARK_FOLDER_DELETE_SUCCESS';
+export const BOOKMARK_FOLDER_DELETE_FAIL = 'BOOKMARK_FOLDER_DELETE_FAIL';
+
+export const BOOKMARK_FOLDER_EDITOR_NAME_CHANGE = 'BOOKMARK_FOLDER_EDITOR_NAME_CHANGE';
+export const BOOKMARK_FOLDER_EDITOR_RESET = 'BOOKMARK_FOLDER_EDITOR_RESET';
+export const BOOKMARK_FOLDER_EDITOR_SETUP = 'BOOKMARK_FOLDER_EDITOR_SETUP';
+
+export const fetchBookmarkFolders = () => (dispatch, getState) => {
+ dispatch(fetchBookmarkFoldersRequest());
+
+ api(getState).get('/api/v1/bookmark_folders')
+ .then(({ data }) => dispatch(fetchBookmarkFoldersSuccess(data)))
+ .catch(err => dispatch(fetchBookmarkFoldersFail(err)));
+};
+
+export const fetchBookmarkFoldersRequest = () => ({
+ type: BOOKMARK_FOLDERS_FETCH_REQUEST,
+});
+
+export const fetchBookmarkFoldersSuccess = folders => ({
+ type: BOOKMARK_FOLDERS_FETCH_SUCCESS,
+ folders,
+});
+
+export const fetchBookmarkFoldersFail = error => ({
+ type: BOOKMARK_FOLDERS_FETCH_FAIL,
+ error,
+});
+
+export const createBookmarkFolder = (name, shouldReset) => (dispatch, getState) => {
+ dispatch(createBookmarkFolderRequest());
+
+ api(getState).post('/api/v1/bookmark_folders', { name }).then(({ data }) => {
+ dispatch(createBookmarkFolderSuccess(data));
+
+ if (shouldReset) {
+ dispatch(resetBookmarkFolderEditor());
+ }
+ }).catch(err => dispatch(createBookmarkFolderFail(err)));
+};
+
+export const createBookmarkFolderRequest = () => ({
+ type: BOOKMARK_FOLDER_CREATE_REQUEST,
+});
+
+export const createBookmarkFolderSuccess = folder => ({
+ type: BOOKMARK_FOLDER_CREATE_SUCCESS,
+ folder,
+});
+
+export const createBookmarkFolderFail = error => ({
+ type: BOOKMARK_FOLDER_CREATE_FAIL,
+ error,
+});
+
+export const updateBookmarkFolder = (id, name, shouldReset) => (dispatch, getState) => {
+ dispatch(updateBookmarkFolderRequest(id));
+
+ api(getState).put(`/api/v1/bookmark_folders/${id}`, { name }).then(({ data }) => {
+ dispatch(updateBookmarkFolderSuccess(data));
+
+ if (shouldReset) {
+ dispatch(resetBookmarkFolderEditor());
+ }
+ }).catch(err => dispatch(updateBookmarkFolderFail(id, err)));
+};
+
+export const updateBookmarkFolderRequest = id => ({
+ type: BOOKMARK_FOLDER_UPDATE_REQUEST,
+ id,
+});
+
+export const updateBookmarkFolderSuccess = folder => ({
+ type: BOOKMARK_FOLDER_UPDATE_SUCCESS,
+ folder,
+});
+
+export const updateBookmarkFolderFail = (id, error) => ({
+ type: BOOKMARK_FOLDER_UPDATE_FAIL,
+ id,
+ error,
+});
+
+export const deleteBookmarkFolder = id => (dispatch, getState) => {
+ dispatch(deleteBookmarkFolderRequest(id));
+
+ api(getState).delete(`/api/v1/bookmark_folders/${id}`)
+ .then(() => dispatch(deleteBookmarkFolderSuccess(id)))
+ .catch(err => dispatch(deleteBookmarkFolderFail(id, err)));
+};
+
+export const deleteBookmarkFolderRequest = id => ({
+ type: BOOKMARK_FOLDER_DELETE_REQUEST,
+ id,
+});
+
+export const deleteBookmarkFolderSuccess = id => ({
+ type: BOOKMARK_FOLDER_DELETE_SUCCESS,
+ id,
+});
+
+export const deleteBookmarkFolderFail = (id, error) => ({
+ type: BOOKMARK_FOLDER_DELETE_FAIL,
+ id,
+ error,
+});
+
+export const submitBookmarkFolderEditor = shouldReset => (dispatch, getState) => {
+ const folderId = getState().getIn(['bookmarkFolderEditor', 'folderId']);
+ const name = getState().getIn(['bookmarkFolderEditor', 'name']);
+
+ if (folderId === null) {
+ dispatch(createBookmarkFolder(name, shouldReset));
+ } else {
+ dispatch(updateBookmarkFolder(folderId, name, shouldReset));
+ }
+};
+
+export const resetBookmarkFolderEditor = () => ({
+ type: BOOKMARK_FOLDER_EDITOR_RESET,
+});
+
+export const setupBookmarkFolderEditor = folderId => (dispatch, getState) => dispatch({
+ type: BOOKMARK_FOLDER_EDITOR_SETUP,
+ folder: getState().getIn(['bookmarkFolders', folderId]),
+});
+
+export const changeBookmarkFolderEditorName = value => ({
+ type: BOOKMARK_FOLDER_EDITOR_NAME_CHANGE,
+ value,
+});
M app/javascript/flavours/glitch/actions/bookmarks.js => app/javascript/flavours/glitch/actions/bookmarks.js +28 -18
@@ 10,82 10,92 @@ export const BOOKMARKED_STATUSES_EXPAND_REQUEST = "BOOKMARKED_STATUSES_EXPAND_RE
export const BOOKMARKED_STATUSES_EXPAND_SUCCESS = "BOOKMARKED_STATUSES_EXPAND_SUCCESS";
export const BOOKMARKED_STATUSES_EXPAND_FAIL = "BOOKMARKED_STATUSES_EXPAND_FAIL";
-export function fetchBookmarkedStatuses() {
+export function fetchBookmarkedStatuses(folderId) {
return (dispatch, getState) => {
- if (getState().getIn(["status_lists", "bookmarks", "isLoading"])) {
+ const key = folderId ? `bookmarks:${folderId}`: "bookmarks";
+
+ if (getState().getIn(["status_lists", key, "isLoading"])) {
return;
}
- dispatch(fetchBookmarkedStatusesRequest());
+ dispatch(fetchBookmarkedStatusesRequest(folderId));
- api(getState).get("/api/v1/bookmarks").then(response => {
+ api(getState).get("/api/v1/bookmarks", { params: { folder_id: folderId } }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedStatuses(response.data));
- dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
+ dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null, folderId));
}).catch(error => {
- dispatch(fetchBookmarkedStatusesFail(error));
+ dispatch(fetchBookmarkedStatusesFail(error, folderId));
});
};
}
-export function fetchBookmarkedStatusesRequest() {
+export function fetchBookmarkedStatusesRequest(folderId) {
return {
type: BOOKMARKED_STATUSES_FETCH_REQUEST,
+ folderId,
};
}
-export function fetchBookmarkedStatusesSuccess(statuses, next) {
+export function fetchBookmarkedStatusesSuccess(statuses, next, folderId) {
return {
type: BOOKMARKED_STATUSES_FETCH_SUCCESS,
statuses,
next,
+ folderId,
};
}
-export function fetchBookmarkedStatusesFail(error) {
+export function fetchBookmarkedStatusesFail(error, folderId) {
return {
type: BOOKMARKED_STATUSES_FETCH_FAIL,
error,
+ folderId,
};
}
-export function expandBookmarkedStatuses() {
+export function expandBookmarkedStatuses(folderId) {
return (dispatch, getState) => {
- const url = getState().getIn(["status_lists", "bookmarks", "next"], null);
+ const key = folderId ? `bookmarks:${folderId}`: "bookmarks";
+
+ const url = getState().getIn(["status_lists", key, "next"], null);
- if (url === null || getState().getIn(["status_lists", "bookmarks", "isLoading"])) {
+ if (url === null || getState().getIn(["status_lists", key, "isLoading"])) {
return;
}
- dispatch(expandBookmarkedStatusesRequest());
+ dispatch(expandBookmarkedStatusesRequest(folderId));
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedStatuses(response.data));
- dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
+ dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null, folderId));
}).catch(error => {
- dispatch(expandBookmarkedStatusesFail(error));
+ dispatch(expandBookmarkedStatusesFail(error, folderId));
});
};
}
-export function expandBookmarkedStatusesRequest() {
+export function expandBookmarkedStatusesRequest(folderId) {
return {
type: BOOKMARKED_STATUSES_EXPAND_REQUEST,
+ folderId,
};
}
-export function expandBookmarkedStatusesSuccess(statuses, next) {
+export function expandBookmarkedStatusesSuccess(statuses, next, folderId) {
return {
type: BOOKMARKED_STATUSES_EXPAND_SUCCESS,
statuses,
next,
+ folderId,
};
}
-export function expandBookmarkedStatusesFail(error) {
+export function expandBookmarkedStatusesFail(error, folderId) {
return {
type: BOOKMARKED_STATUSES_EXPAND_FAIL,
error,
+ folderId,
};
}
M app/javascript/flavours/glitch/actions/compose.js => app/javascript/flavours/glitch/actions/compose.js +12 -2
@@ 89,6 89,9 @@ export const COMPOSE_SET_STATUS = "COMPOSE_SET_STATUS";
const messages = defineMessages({
uploadErrorLimit: { id: "upload_error.limit", defaultMessage: "File upload limit exceeded." },
uploadErrorPoll: { id: "upload_error.poll", defaultMessage: "File upload not allowed with polls." },
+ open: { id: "compose.published.open", defaultMessage: "Open" },
+ published: { id: "compose.published.body", defaultMessage: "Post published." },
+ saved: { id: "compose.saved.body", defaultMessage: "Post saved." },
});
export const ensureComposeIsVisible = (getState, routerHistory) => {
@@ 262,6 265,13 @@ export function submitCompose(routerHistory) {
} else if (statusId === null && response.data.visibility === "direct") {
insertIfOnline("direct");
}
+
+ dispatch(showAlert({
+ message: statusId === null ? messages.published : messages.saved,
+ action: messages.open,
+ dismissAfter: 10000,
+ onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
+ }));
}).catch(function (error) {
dispatch(submitComposeFail(error));
});
@@ 304,12 314,12 @@ export function uploadCompose(files) {
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
if (files.length + media.size + pending > uploadLimit) {
- dispatch(showAlert(undefined, messages.uploadErrorLimit));
+ dispatch(showAlert({ message: messages.uploadErrorLimit }));
return;
}
if (getState().getIn(["compose", "poll"])) {
- dispatch(showAlert(undefined, messages.uploadErrorPoll));
+ dispatch(showAlert({ message: messages.uploadErrorPoll }));
return;
}
M app/javascript/flavours/glitch/actions/interactions.js => app/javascript/flavours/glitch/actions/interactions.js +27 -4
@@ 1,7 1,11 @@
+import { defineMessages } from "react-intl";
+
import api, { getLinks } from "../api";
import { fetchRelationships } from "./accounts";
+import { showAlert } from "./alerts";
import { importFetchedAccounts, importFetchedStatus } from "./importer";
+import { openModal } from "./modal";
export const REBLOG_REQUEST = "REBLOG_REQUEST";
export const REBLOG_SUCCESS = "REBLOG_SUCCESS";
@@ 51,6 55,14 @@ export const UNBOOKMARK_REQUEST = "UNBOOKMARKED_REQUEST";
export const UNBOOKMARK_SUCCESS = "UNBOOKMARKED_SUCCESS";
export const UNBOOKMARK_FAIL = "UNBOOKMARKED_FAIL";
+const messages = defineMessages({
+ bookmarkAdded: { id: 'status.bookmarked', defaultMessage: 'Bookmark added.' },
+ folderChanged: { id: 'status.bookmark_folder_changed', defaultMessage: 'Changed folder' },
+ view: { id: 'toast.view', defaultMessage: 'View' },
+ selectFolder: { id: 'status.bookmark.select_folder', defaultMessage: 'Select folder' },
+
+});
+
export function reblog(status, visibility) {
return function (dispatch, getState) {
dispatch(reblogRequest(status));
@@ 193,13 205,23 @@ export function unfavouriteFail(status, error) {
};
}
-export function bookmark(status) {
+export function bookmark(status, folder_id, routerHistory) {
return function (dispatch, getState) {
dispatch(bookmarkRequest(status));
- api(getState).post(`/api/v1/statuses/${status.get("id")}/bookmark`).then(function (response) {
+ return api(getState).post(`/api/v1/statuses/${status.get("id")}/bookmark`, { folder_id }).then(function (response) {
dispatch(importFetchedStatus(response.data));
- dispatch(bookmarkSuccess(status));
+ dispatch(bookmarkSuccess(status, folder_id));
+
+ dispatch(showAlert({
+ message: folder_id !== undefined ? messages.folderChanged : messages.bookmarkAdded,
+ action: folder_id !== undefined ? messages.view : messages.selectFolder,
+ dismissAfter: 10000,
+ onClick: () => folder_id !== undefined ? routerHistory.push(`/bookmarks/${folder_id}`) : dispatch(openModal({
+ modalType: 'SELECT_BOOKMARK_FOLDER',
+ modalProps: { statusId: status.get('id') },
+ })),
+ }));
}).catch(function (error) {
dispatch(bookmarkFail(status, error));
});
@@ 226,10 248,11 @@ export function bookmarkRequest(status) {
};
}
-export function bookmarkSuccess(status) {
+export function bookmarkSuccess(status, folderId) {
return {
type: BOOKMARK_SUCCESS,
status: status,
+ folderId,
};
}
M app/javascript/flavours/glitch/components/status.jsx => app/javascript/flavours/glitch/components/status.jsx +4 -2
@@ 96,11 96,13 @@ class Status extends ImmutablePureComponent {
onToggleHidden: PropTypes.func,
onTranslate: PropTypes.func,
onInteractionModal: PropTypes.func,
+ onChangeBookmarkFolder: PropTypes.func,
muted: PropTypes.bool,
hidden: PropTypes.bool,
unread: PropTypes.bool,
prepend: PropTypes.string,
withDismiss: PropTypes.bool,
+ fromBookmarks: PropTypes.bool,
onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func,
getScrollPosition: PropTypes.func,
@@ 457,8 459,8 @@ class Status extends ImmutablePureComponent {
this.props.onReblog(this.props.status, e);
};
- handleHotkeyBookmark = e => {
- this.props.onBookmark(this.props.status, e);
+ handleHotkeyBookmark = () => {
+ this.props.onBookmark(this.props.status, undefined, this.context.router.history);
};
handleHotkeyMention = e => {
M app/javascript/flavours/glitch/components/status_action_bar.jsx => app/javascript/flavours/glitch/components/status_action_bar.jsx +15 -3
@@ 48,6 48,7 @@ const messages = defineMessages({
edited: { id: "status.edited", defaultMessage: "Edited {date}" },
filter: { id: "status.filter", defaultMessage: "Filter this post" },
openOriginalPage: { id: "account.open_original_page", defaultMessage: "Open original page" },
+ changeBookmarkFolder: { id: "status.bookmark.change_folder", defaultMessage: "Change bookmark folder" },
});
class StatusActionBar extends ImmutablePureComponent {
@@ 75,9 76,11 @@ class StatusActionBar extends ImmutablePureComponent {
onFilter: PropTypes.func,
onAddFilter: PropTypes.func,
onInteractionModal: PropTypes.func,
+ onChangeBookmarkFolder: PropTypes.func,
withDismiss: PropTypes.bool,
withCounters: PropTypes.bool,
showReplyCount: PropTypes.bool,
+ fromBookmarks: PropTypes.bool,
scrollKey: PropTypes.string,
intl: PropTypes.object.isRequired,
};
@@ 127,8 130,8 @@ class StatusActionBar extends ImmutablePureComponent {
}
};
- handleBookmarkClick = (e) => {
- this.props.onBookmark(this.props.status, e);
+ handleBookmarkClick = () => {
+ this.props.onBookmark(this.props.status, undefined, this.context.router.history);
};
handleDeleteClick = () => {
@@ 197,8 200,12 @@ class StatusActionBar extends ImmutablePureComponent {
this.props.onAddFilter(this.props.status);
};
+ handleChangeBookmarkFolder = () => {
+ this.props.onChangeBookmarkFolder(this.props.status);
+ };
+
render () {
- const { status, intl, withDismiss, withCounters, showReplyCount, scrollKey } = this.props;
+ const { status, intl, withDismiss, withCounters, showReplyCount, fromBookmarks, scrollKey } = this.props;
const { permissions, signedIn } = this.context.identity;
const mutingConversation = status.get("muted");
@@ 212,6 219,11 @@ class StatusActionBar extends ImmutablePureComponent {
let replyIcon;
let replyTitle;
+ if (status.get('bookmarked') && fromBookmarks) {
+ menu.push({ text: intl.formatMessage(messages.changeBookmarkFolder), action: this.handleChangeBookmarkFolder });
+ menu.push(null);
+ }
+
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
if (publicStatus && isRemote) {
M app/javascript/flavours/glitch/components/status_list.jsx => app/javascript/flavours/glitch/components/status_list.jsx +3 -0
@@ 28,6 28,7 @@ export default class StatusList extends ImmutablePureComponent {
emptyMessage: PropTypes.node,
alwaysPrepend: PropTypes.bool,
withCounters: PropTypes.bool,
+ fromBookmarks: PropTypes.bool,
timelineId: PropTypes.string.isRequired,
lastId: PropTypes.string,
regex: PropTypes.string,
@@ 107,6 108,7 @@ export default class StatusList extends ImmutablePureComponent {
contextType={timelineId}
scrollKey={this.props.scrollKey}
withCounters={this.props.withCounters}
+ fromBookmarks={this.props.fromBookmarks}
/>
))
) : null;
@@ 122,6 124,7 @@ export default class StatusList extends ImmutablePureComponent {
contextType={timelineId}
scrollKey={this.props.scrollKey}
withCounters={this.props.withCounters}
+ fromBookmarks={this.props.fromBookmarks}
/>
)).concat(scrollableContent);
}
M app/javascript/flavours/glitch/containers/status_container.js => app/javascript/flavours/glitch/containers/status_container.js +9 -2
@@ 135,11 135,11 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
});
},
- onBookmark (status) {
+ onBookmark (status, folderId, router) {
if (status.get("bookmarked")) {
dispatch(unbookmark(status));
} else {
- dispatch(bookmark(status));
+ dispatch(bookmark(status, folderId, router));
}
},
@@ 298,6 298,13 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
}));
},
+ onChangeBookmarkFolder (status) {
+ dispatch(openModal({
+ modalType: 'SELECT_BOOKMARK_FOLDER',
+ modalProps: { statusId: status.get('id') },
+ }));
+ },
+
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
A app/javascript/flavours/glitch/features/bookmark_folder/index.jsx => app/javascript/flavours/glitch/features/bookmark_folder/index.jsx +142 -0
@@ 0,0 1,142 @@
+import PropTypes from 'prop-types';
+
+import { injectIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { debounce } from 'lodash';
+
+import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from 'flavours/glitch/actions/bookmarks';
+import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
+import ColumnHeader from 'flavours/glitch/components/column_header';
+import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
+import StatusList from 'flavours/glitch/components/status_list';
+import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
+import Column from 'flavours/glitch/features/ui/components/column';
+import { getStatusList } from 'flavours/glitch/selectors';
+
+const mapStateToProps = (state, props) => {
+ const key = `bookmarks:${props.params.folderId}`;
+ return {
+ folder: state.getIn(['bookmarkFolders', props.params.folderId]),
+ statusIds: getStatusList(state, key),
+ isLoading: state.getIn(['status_lists', key, 'isLoading'], true),
+ hasMore: !!state.getIn(['status_lists', key, 'next']),
+ };
+};
+
+class BookmarkFolder extends ImmutablePureComponent {
+
+ static propTypes = {
+ dispatch: PropTypes.func.isRequired,
+ statusIds: ImmutablePropTypes.list.isRequired,
+ intl: PropTypes.object.isRequired,
+ columnId: PropTypes.string,
+ multiColumn: PropTypes.bool,
+ hasMore: PropTypes.bool,
+ isLoading: PropTypes.bool,
+ };
+
+ UNSAFE_componentWillMount () {
+ this.props.dispatch(fetchBookmarkedStatuses(this.props.params.folderId));
+ }
+
+ UNSAFE_componentWillReceiveProps (nextProps) {
+ const { folderId } = nextProps.params;
+
+ if (folderId !== this.props.params.folderId) {
+
+ this.props.dispatch(fetchBookmarkedStatuses(folderId));
+
+ }
+ }
+
+ handlePin = () => {
+ const { columnId, dispatch } = this.props;
+
+ if (columnId) {
+ dispatch(removeColumn(columnId));
+ } else {
+ dispatch(addColumn('BOOKMARK_FOLDER', { folderId: this.props.params.folderId }));
+ }
+ };
+
+ handleMove = (dir) => {
+ const { columnId, dispatch } = this.props;
+ dispatch(moveColumn(columnId, dir));
+ };
+
+ handleHeaderClick = () => {
+ this.column.scrollTop();
+ };
+
+ setRef = c => {
+ this.column = c;
+ };
+
+ handleLoadMore = debounce(() => {
+ this.props.dispatch(expandBookmarkedStatuses(this.props.params.folderId));
+ }, 300, { leading: true });
+
+ render () {
+ const { statusIds, columnId, multiColumn, folder, hasMore, isLoading, params } = this.props;
+ const { folderId } = params;
+ const pinned = !!columnId;
+ const name = folder ? folder.get('name') : folderId;
+
+ const emptyMessage = <FormattedMessage id='empty_column.bookmarked_statuses.folder' defaultMessage="You don't have any bookmarked posts in this folder yet. When you ad one, it will show up here." />;
+
+ if (typeof folder === 'undefined') {
+ return (
+ <Column>
+ <div className='scrollable'>
+ <LoadingIndicator />
+ </div>
+ </Column>
+ );
+ } else if (folder === false) {
+ return (
+ <BundleColumnError multiColumn={multiColumn} errorType='routing' />
+ );
+ }
+
+ return (
+ <Column bindToDocument={!multiColumn} ref={this.setRef} label={name}>
+ <ColumnHeader
+ icon='bookmark'
+ title={name}
+ onPin={this.handlePin}
+ onMove={this.handleMove}
+ onClick={this.handleHeaderClick}
+ pinned={pinned}
+ multiColumn={multiColumn}
+ showBackButton
+ />
+
+ <StatusList
+ trackScroll={!pinned}
+ statusIds={statusIds}
+ scrollKey={`bookmarked_statuses:${folderId}-${columnId}`}
+ hasMore={hasMore}
+ isLoading={isLoading}
+ onLoadMore={this.handleLoadMore}
+ emptyMessage={emptyMessage}
+ bindToDocument={!multiColumn}
+ fromBookmarks
+ />
+
+ <Helmet>
+ <title>{name}</title>
+ <meta name='robots' content='noindex' />
+ </Helmet>
+ </Column>
+ );
+ }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(BookmarkFolder));
A app/javascript/flavours/glitch/features/bookmark_folders/index.jsx => app/javascript/flavours/glitch/features/bookmark_folders/index.jsx +96 -0
@@ 0,0 1,96 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+
+import { fetchBookmarkFolders } from 'flavours/glitch/actions/bookmark_folders';
+import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim';
+import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
+import ScrollableList from 'flavours/glitch/components/scrollable_list';
+import Column from 'flavours/glitch/features/ui/components/column';
+import ColumnLink from 'flavours/glitch/features/ui/components/column_link';
+import ColumnSubheading from 'flavours/glitch/features/ui/components/column_subheading';
+
+import NewFolderForm from '../select_bookmark_folder_modal/components/new_folder_form';
+
+const messages = defineMessages({
+ heading: { id: 'column.bookmark_folders', defaultMessage: 'Bookmark folders' },
+ subheading: { id: 'bookmark_folders.subheading', defaultMessage: 'Your bookmark folders' },
+});
+
+const getOrderedBookmarkFolders = createSelector([state => state.get('bookmarkFolders')], folders => {
+ if (!folders) {
+ return folders;
+ }
+
+ return folders.toList().filter(item => !!item).sort((a, b) => a.get('name').localeCompare(b.get('name')));
+});
+
+const mapStateToProps = state => ({
+ folders: getOrderedBookmarkFolders(state),
+});
+
+class BookmarkFolders extends ImmutablePureComponent {
+
+ static propTypes = {
+ params: PropTypes.object.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ folders: ImmutablePropTypes.list,
+ intl: PropTypes.object.isRequired,
+ multiColumn: PropTypes.bool,
+ };
+
+ UNSAFE_componentWillMount () {
+ this.props.dispatch(fetchBookmarkFolders());
+ }
+
+ render () {
+ const { intl, folders, multiColumn } = this.props;
+
+ if (!folders) {
+ return (
+ <Column>
+ <LoadingIndicator />
+ </Column>
+ );
+ }
+
+ const children = [
+ <ColumnLink key='all' to='/bookmarks' icon='bookmark' text={<FormattedMessage id='bookmark_folders.all' defaultMessage='All bookmarks' />} />,
+ ];
+
+ for (const folder of folders) {
+ children.push(<ColumnLink key={folder.get('id')} to={`/bookmarks/${folder.get('id')}`} icon='folder' text={folder.get('name')} />)
+ }
+
+ return (
+ <Column bindToDocument={!multiColumn} icon='bars' heading={intl.formatMessage(messages.heading)}>
+ <ColumnBackButtonSlim />
+
+ <NewFolderForm />
+
+ <ColumnSubheading text={intl.formatMessage(messages.subheading)} />
+ <ScrollableList
+ scrollKey='bookmark_folders'
+ bindToDocument={!multiColumn}
+ >
+ {children}
+ </ScrollableList>
+
+ <Helmet>
+ <title>{intl.formatMessage(messages.heading)}</title>
+ <meta name='robots' content='noindex' />
+ </Helmet>
+ </Column>
+ );
+ }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(BookmarkFolders));
M app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx => app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx +1 -0
@@ 98,6 98,7 @@ class Bookmarks extends ImmutablePureComponent {
onLoadMore={this.handleLoadMore}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
+ fromBookmarks
/>
<Helmet>
M app/javascript/flavours/glitch/features/getting_started/index.jsx => app/javascript/flavours/glitch/features/getting_started/index.jsx +6 -1
@@ 11,6 11,7 @@ import { connect } from "react-redux";
import { createSelector } from "reselect";
import { fetchFollowRequests } from "flavours/glitch/actions/accounts";
+import { fetchBookmarkFolders } from "flavours/glitch/actions/bookmark_folders";
import { fetchLists } from "flavours/glitch/actions/lists";
import { openModal } from "flavours/glitch/actions/modal";
import Column from "flavours/glitch/features/ui/components/column";
@@ 69,6 70,7 @@ const makeMapStateToProps = () => {
const mapDispatchToProps = dispatch => ({
fetchFollowRequests: () => dispatch(fetchFollowRequests()),
fetchLists: () => dispatch(fetchLists()),
+ fetchBookmarkFolders: () => dispatch(fetchBookmarkFolders()),
openSettings: () => dispatch(openModal({
modalType: "SETTINGS",
modalProps: {},
@@ 102,15 104,17 @@ class GettingStarted extends ImmutablePureComponent {
unreadNotifications: PropTypes.number,
lists: ImmutablePropTypes.list,
fetchLists: PropTypes.func.isRequired,
+ fetchBookmarkFolders: PropTypes.func.isRequired,
openSettings: PropTypes.func.isRequired,
};
UNSAFE_componentWillMount () {
this.props.fetchLists();
+ this.props.fetchBookmarkFolders();
}
componentDidMount () {
- const { fetchFollowRequests } = this.props;
+ const { fetchFollowRequests, fetchBookmarkFolders } = this.props;
const { signedIn } = this.context.identity;
if (!signedIn) {
@@ 118,6 122,7 @@ class GettingStarted extends ImmutablePureComponent {
}
fetchFollowRequests();
+ fetchBookmarkFolders();
}
render () {
M app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js => app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js +2 -2
@@ 33,7 33,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (permission === "granted") {
dispatch(changePushNotifications(path.slice(1), checked));
} else {
- dispatch(showAlert(undefined, messages.permissionDenied));
+ dispatch(showAlert({ message: messages.permissionDenied }));
}
}));
} else {
@@ 48,7 48,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (permission === "granted") {
dispatch(changeSetting(["notifications", ...path], checked));
} else {
- dispatch(showAlert(undefined, messages.permissionDenied));
+ dispatch(showAlert({ message: messages.permissionDenied }));
}
}));
} else {
A app/javascript/flavours/glitch/features/select_bookmark_folder_modal/components/bookmark_folder.jsx => app/javascript/flavours/glitch/features/select_bookmark_folder_modal/components/bookmark_folder.jsx +71 -0
@@ 0,0 1,71 @@
+import PropTypes from 'prop-types';
+
+import { FormattedMessage, injectIntl } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { bookmark } from 'flavours/glitch/actions/interactions';
+import { closeModal } from 'flavours/glitch/actions/modal';
+import { RadioButton } from 'flavours/glitch/components/radio_button';
+
+const mapStateToProps = (state, { statusId }) => ({
+ status: state.getIn(['statuses', statusId]),
+});
+
+const mapDispatchToProps = (dispatch, { folder }) => ({
+ onChange: (status, router) => {
+ dispatch(bookmark(status, folder ? folder.get('id') : null, router)).then(() => {
+ dispatch(closeModal({
+ modalType: 'SELECT_BOOKMARK_FOLDER',
+ }));
+ }).catch(() => {});
+ },
+});
+
+class BookmarkFolder extends ImmutablePureComponent {
+
+ static contextTypes = {
+ router: PropTypes.object,
+ };
+
+ static propTypes = {
+ status: ImmutablePropTypes.map.isRequired,
+ folder: ImmutablePropTypes.map,
+ statusId: PropTypes.string.isRequired,
+ statusFolderId: PropTypes.string,
+ intl: PropTypes.object.isRequired,
+ onChange: PropTypes.func.isRequired,
+ };
+
+ static defaultProps = {
+ added: false,
+ };
+
+ handleChange = () => {
+ const { onChange, status } = this.props;
+ onChange(status, this.context.router.history);
+ };
+
+ render () {
+ const { folder, statusFolderId } = this.props;
+
+ return (
+ <div className='bookmark-folder'>
+ <div className='bookmark-folder__wrapper'>
+ <RadioButton
+ name='folder_id'
+ value={statusFolderId || ''}
+ label={folder ? folder.get('name') : <FormattedMessage id='bookmark_folders.uncategorized' defaultMessage='Uncategorized' />}
+ checked={statusFolderId === (folder ? folder.get('id') : null)}
+ onChange={this.handleChange}
+ />
+ </div>
+ </div>
+ );
+ }
+
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(BookmarkFolder));
A app/javascript/flavours/glitch/features/select_bookmark_folder_modal/components/new_folder_form.jsx => app/javascript/flavours/glitch/features/select_bookmark_folder_modal/components/new_folder_form.jsx +81 -0
@@ 0,0 1,81 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import { connect } from 'react-redux';
+
+import { changeBookmarkFolderEditorName, submitBookmarkFolderEditor } from 'flavours/glitch/actions/bookmark_folders';
+import { IconButton } from 'flavours/glitch/components/icon_button';
+
+const messages = defineMessages({
+ label: { id: 'bookmark_folders.new.name_placeholder', defaultMessage: 'New folder name' },
+ title: { id: 'bookmark_folders.new.create', defaultMessage: 'Add folder' },
+});
+
+const mapStateToProps = state => ({
+ value: state.getIn(['bookmarkFolderEditor', 'name']),
+ disabled: state.getIn(['bookmarkFolderEditor', 'isSubmitting']),
+});
+
+const mapDispatchToProps = dispatch => ({
+ onChange: value => dispatch(changeBookmarkFolderEditorName(value)),
+ onSubmit: () => dispatch(submitBookmarkFolderEditor(true)),
+});
+
+class NewFolderForm extends PureComponent {
+
+ static propTypes = {
+ value: PropTypes.string.isRequired,
+ disabled: PropTypes.bool,
+ intl: PropTypes.object.isRequired,
+ onChange: PropTypes.func.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+ };
+
+ handleChange = e => {
+ this.props.onChange(e.target.value);
+ };
+
+ handleSubmit = e => {
+ e.preventDefault();
+ this.props.onSubmit();
+ };
+
+ handleClick = () => {
+ this.props.onSubmit();
+ };
+
+ render () {
+ const { value, disabled, intl } = this.props;
+
+ const label = intl.formatMessage(messages.label);
+ const title = intl.formatMessage(messages.title);
+
+ return (
+ <form className='column-inline-form' onSubmit={this.handleSubmit}>
+ <label>
+ <span style={{ display: 'none' }}>{label}</span>
+
+ <input
+ className='setting-text'
+ value={value}
+ disabled={disabled}
+ onChange={this.handleChange}
+ placeholder={label}
+ />
+ </label>
+
+ <IconButton
+ disabled={disabled || !value}
+ icon='plus'
+ title={title}
+ onClick={this.handleClick}
+ />
+ </form>
+ );
+ }
+
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(NewFolderForm));
A app/javascript/flavours/glitch/features/select_bookmark_folder_modal/index.jsx => app/javascript/flavours/glitch/features/select_bookmark_folder_modal/index.jsx +73 -0
@@ 0,0 1,73 @@
+import PropTypes from 'prop-types';
+
+import { injectIntl } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+
+import { fetchBookmarkFolders } from 'flavours/glitch/actions/bookmark_folders';
+import { makeGetStatus } from 'flavours/glitch/selectors';
+
+import BookmarkFolder from './components/bookmark_folder';
+import NewFolderForm from './components/new_folder_form';
+
+const getOrderedBookmarkFolders = createSelector([state => state.get('bookmarkFolders')], folders => {
+ if (!folders) {
+ return folders;
+ }
+
+ return folders.toList().filter(item => !!item).sort((a, b) => a.get('name').localeCompare(b.get('name')));
+});
+
+const makeMapStateToProps = () => {
+ const getStatus = makeGetStatus();
+
+ const mapStateToProps = (state, { statusId }) => ({
+ folders: getOrderedBookmarkFolders(state),
+ status: getStatus(state, { id: statusId }),
+ });
+
+ return mapStateToProps;
+};
+
+const mapDispatchToProps = (dispatch) => ({
+ onInitialize () {
+ dispatch(fetchBookmarkFolders());
+ }
+});
+
+class SelectBookmarkFolderModal extends ImmutablePureComponent {
+
+ static propTypes = {
+ statusId: PropTypes.string.isRequired,
+ onClose: PropTypes.func.isRequired,
+ intl: PropTypes.object.isRequired,
+ onInitialize: PropTypes.func.isRequired,
+ folders: ImmutablePropTypes.list.isRequired,
+ };
+
+ componentDidMount () {
+ const { onInitialize } = this.props;
+ onInitialize();
+ }
+
+ render () {
+ const { folders, status } = this.props;
+
+ return (
+ <div className='modal-root__modal select-bookmark-folder'>
+ <NewFolderForm />
+
+ <div className='select-bookmark-folder__folders'>
+ <BookmarkFolder statusId={status.get('id')} statusFolderId={status.get('bookmark_folder')} />
+ {folders.map(folder => <BookmarkFolder key={folder.get('id')} folder={folder} statusId={status.get('id')} statusFolderId={status.get('bookmark_folder')} />)}
+ </div>
+ </div>
+ );
+ }
+
+}
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(injectIntl(SelectBookmarkFolderModal));
M app/javascript/flavours/glitch/features/status/components/action_bar.jsx => app/javascript/flavours/glitch/features/status/components/action_bar.jsx +2 -2
@@ 81,8 81,8 @@ class ActionBar extends PureComponent {
this.props.onFavourite(this.props.status, e);
};
- handleBookmarkClick = (e) => {
- this.props.onBookmark(this.props.status, e);
+ handleBookmarkClick = () => {
+ this.props.onBookmark(this.props.status, undefined, this.context.router.history);
};
handleDeleteClick = () => {
M app/javascript/flavours/glitch/features/status/index.jsx => app/javascript/flavours/glitch/features/status/index.jsx +1 -1
@@ 382,7 382,7 @@ class Status extends ImmutablePureComponent {
if (status.get("bookmarked")) {
this.props.dispatch(unbookmark(status));
} else {
- this.props.dispatch(bookmark(status));
+ this.props.dispatch(bookmark(status, undefined, this.context.router.history));
}
};
M app/javascript/flavours/glitch/features/ui/components/columns_area.jsx => app/javascript/flavours/glitch/features/ui/components/columns_area.jsx +2 -0
@@ 21,6 21,7 @@ import {
BookmarkedStatuses,
ListTimeline,
Directory,
+ BookmarkFolder,
} from "../util/async-components";
import BundleColumnError from "./bundle_column_error";
@@ 40,6 41,7 @@ const componentMap = {
"DIRECT": DirectTimeline,
"FAVOURITES": FavouritedStatuses,
"BOOKMARKS": BookmarkedStatuses,
+ "BOOKMARK_FOLDER": BookmarkFolder,
"LIST": ListTimeline,
"DIRECTORY": Directory,
};
M app/javascript/flavours/glitch/features/ui/components/modal_root.jsx => app/javascript/flavours/glitch/features/ui/components/modal_root.jsx +2 -0
@@ 19,6 19,7 @@ import {
InteractionModal,
SubscribedLanguagesModal,
ClosedRegistrationsModal,
+ SelectBookmarkFolderModal,
} from "flavours/glitch/features/ui/util/async-components";
import { getScrollbarWidth } from "flavours/glitch/utils/scrollbar";
@@ 64,6 65,7 @@ export const MODAL_COMPONENTS = {
"SUBSCRIBED_LANGUAGES": SubscribedLanguagesModal,
"INTERACTION": InteractionModal,
"CLOSED_REGISTRATIONS": ClosedRegistrationsModal,
+ "SELECT_BOOKMARK_FOLDER": SelectBookmarkFolderModal,
};
export default class ModalRoot extends PureComponent {
M app/javascript/flavours/glitch/features/ui/containers/notifications_container.js => app/javascript/flavours/glitch/features/ui/containers/notifications_container.js +19 -18
@@ 7,26 7,27 @@ import { NotificationStack } from "react-notification";
import { dismissAlert } from "flavours/glitch/actions/alerts";
import { getAlerts } from "flavours/glitch/selectors";
-const mapStateToProps = (state, { intl }) => {
- const notifications = getAlerts(state);
+const formatIfNeeded = (intl, message, values) => {
+ if (typeof message === 'object') {
+ return intl.formatMessage(message, values);
+ }
- notifications.forEach(notification => ["title", "message"].forEach(key => {
- const value = notification[key];
-
- if (typeof value === "object") {
- notification[key] = intl.formatMessage(value, notification[`${key}_values`]);
- }
- }));
-
- return { notifications };
+ return message;
};
-const mapDispatchToProps = (dispatch) => {
- return {
- onDismiss: alert => {
- dispatch(dismissAlert(alert));
- },
- };
-};
+const mapStateToProps = (state, { intl }) => ({
+ notifications: getAlerts(state).map(alert => ({
+ ...alert,
+ action: formatIfNeeded(intl, alert.action, alert.values),
+ title: formatIfNeeded(intl, alert.title, alert.values),
+ message: formatIfNeeded(intl, alert.message, alert.values),
+ })),
+});
+
+const mapDispatchToProps = (dispatch) => ({
+ onDismiss (alert) {
+ dispatch(dismissAlert(alert));
+ },
+});
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationStack));
M app/javascript/flavours/glitch/features/ui/index.jsx => app/javascript/flavours/glitch/features/ui/index.jsx +4 -0
@@ 64,6 64,8 @@ import {
FollowRecommendations,
About,
PrivacyPolicy,
+ BookmarkFolders,
+ BookmarkFolder,
} from "./util/async-components";
import { WrappedSwitch, WrappedRoute } from "./util/react_router_helpers";
// Dummy import, to make sure that <Status /> ends up in the application bundle.
@@ 212,7 214,9 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/notifications' component={Notifications} content={children} />
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />
+ <WrappedRoute path='/bookmarks/:folderId' component={BookmarkFolder} content={children} />
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
+ <WrappedRoute path='/bookmark_folders' component={BookmarkFolders} content={children} />
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
<WrappedRoute path='/start' component={FollowRecommendations} content={children} />
M app/javascript/flavours/glitch/features/ui/util/async-components.js => app/javascript/flavours/glitch/features/ui/util/async-components.js +12 -0
@@ 205,3 205,15 @@ export function About () {
export function PrivacyPolicy () {
return import(/*webpackChunkName: "features/glitch/async/privacy_policy" */"flavours/glitch/features/privacy_policy");
}
+
+export function SelectBookmarkFolderModal () {
+ return import(/*webpackChunkName: "flavours/glitch/async/modals/select_bookmark_folder_modal" */'flavours/glitch/features/select_bookmark_folder_modal');
+}
+
+export function BookmarkFolders () {
+ return import(/*webpackChunkName: "flavours/glitch/async/bookmark_folders" */'flavours/glitch/features/bookmark_folders');
+}
+
+export function BookmarkFolder () {
+ return import(/*webpackChunkName: "flavours/glitch/async/bookmark_folder" */'flavours/glitch/features/bookmark_folder');
+}
M app/javascript/flavours/glitch/reducers/alerts.js => app/javascript/flavours/glitch/reducers/alerts.js +17 -14
@@ 1,4 1,4 @@
-import { Map as ImmutableMap, List as ImmutableList } from "immutable";
+import { List as ImmutableList } from "immutable";
import {
ALERT_SHOW,
@@ 8,20 8,23 @@ import {
const initialState = ImmutableList([]);
+let id = 0;
+
+const addAlert = (state, alert) =>
+ state.push({
+ key: id++,
+ ...alert,
+ });
+
export default function alerts(state = initialState, action) {
switch(action.type) {
- case ALERT_SHOW:
- return state.push(ImmutableMap({
- key: state.size > 0 ? state.last().get("key") + 1 : 0,
- title: action.title,
- message: action.message,
- message_values: action.message_values,
- }));
- case ALERT_DISMISS:
- return state.filterNot(item => item.get("key") === action.alert.key);
- case ALERT_CLEAR:
- return state.clear();
- default:
- return state;
+ case ALERT_SHOW:
+ return addAlert(state, action.alert);
+ case ALERT_DISMISS:
+ return state.filterNot(item => item.key === action.alert.key);
+ case ALERT_CLEAR:
+ return state.clear();
+ default:
+ return state;
}
}
A app/javascript/flavours/glitch/reducers/bookmark_folder_editor.js => app/javascript/flavours/glitch/reducers/bookmark_folder_editor.js +54 -0
@@ 0,0 1,54 @@
+import { Map as ImmutableMap } from 'immutable';
+
+import {
+ BOOKMARK_FOLDER_CREATE_REQUEST,
+ BOOKMARK_FOLDER_CREATE_FAIL,
+ BOOKMARK_FOLDER_CREATE_SUCCESS,
+ BOOKMARK_FOLDER_UPDATE_REQUEST,
+ BOOKMARK_FOLDER_UPDATE_FAIL,
+ BOOKMARK_FOLDER_UPDATE_SUCCESS,
+ BOOKMARK_FOLDER_EDITOR_RESET,
+ BOOKMARK_FOLDER_EDITOR_SETUP,
+ BOOKMARK_FOLDER_EDITOR_NAME_CHANGE,
+} from '../actions/bookmark_folders';
+
+const initialState = ImmutableMap({
+ folderId: null,
+ isSubmitting: false,
+ isChanged: false,
+ name: '',
+});
+
+export default function listEditorReducer(state = initialState, action) {
+ switch(action.type) {
+ case BOOKMARK_FOLDER_EDITOR_RESET:
+ return initialState;
+ case BOOKMARK_FOLDER_EDITOR_SETUP:
+ return state.withMutations(map => {
+ map.set('folderId', action.folder.get('id'));
+ map.set('folder', action.folder.get('title'));
+ });
+ case BOOKMARK_FOLDER_EDITOR_NAME_CHANGE:
+ return state.withMutations(map => {
+ map.set('name', action.value);
+ map.set('isChanged', true);
+ });
+ case BOOKMARK_FOLDER_CREATE_REQUEST:
+ case BOOKMARK_FOLDER_UPDATE_REQUEST:
+ return state.withMutations(map => {
+ map.set('isSubmitting', true);
+ map.set('isChanged', false);
+ });
+ case BOOKMARK_FOLDER_CREATE_FAIL:
+ case BOOKMARK_FOLDER_UPDATE_FAIL:
+ return state.set('isSubmitting', false);
+ case BOOKMARK_FOLDER_CREATE_SUCCESS:
+ case BOOKMARK_FOLDER_UPDATE_SUCCESS:
+ return state.withMutations(map => {
+ map.set('isSubmitting', false);
+ map.set('folderId', action.folder.id);
+ });
+ default:
+ return state;
+ }
+}
A app/javascript/flavours/glitch/reducers/bookmark_folders.js => app/javascript/flavours/glitch/reducers/bookmark_folders.js +34 -0
@@ 0,0 1,34 @@
+import { Map as ImmutableMap, fromJS } from 'immutable';
+
+import {
+ BOOKMARK_FOLDERS_FETCH_SUCCESS,
+ BOOKMARK_FOLDER_CREATE_SUCCESS,
+ BOOKMARK_FOLDER_UPDATE_SUCCESS,
+ BOOKMARK_FOLDER_DELETE_SUCCESS,
+} from '../actions/bookmark_folders';
+
+const initialState = ImmutableMap();
+
+const normalizeBookmarkFolder = (state, folder) => state.set(folder.id, fromJS(folder));
+
+const normalizeBookmarkFolders = (state, folders) => {
+ folders.forEach(folder => {
+ state = normalizeBookmarkFolder(state, folder);
+ });
+
+ return state;
+};
+
+export default function bookmarkFolders(state = initialState, action) {
+ switch(action.type) {
+ case BOOKMARK_FOLDER_CREATE_SUCCESS:
+ case BOOKMARK_FOLDER_UPDATE_SUCCESS:
+ return normalizeBookmarkFolder(state, action.folder);
+ case BOOKMARK_FOLDERS_FETCH_SUCCESS:
+ return normalizeBookmarkFolders(state, action.folders);
+ case BOOKMARK_FOLDER_DELETE_SUCCESS:
+ return state.set(action.id, false);
+ default:
+ return state;
+ }
+}
M app/javascript/flavours/glitch/reducers/index.ts => app/javascript/flavours/glitch/reducers/index.ts +4 -0
@@ 10,6 10,8 @@ import accounts_map from "./accounts_map";
import alerts from "./alerts";
import announcements from "./announcements";
import blocks from "./blocks";
+import bookmarkFolders from "./bookmark_folders";
+import bookmarkFolderEditor from "./bookmark_folder_editor";
import boosts from "./boosts";
import compose from "./compose";
import contexts from "./contexts";
@@ 49,6 51,8 @@ import user_lists from "./user_lists";
const reducers = {
announcements,
+ bookmarkFolders,
+ bookmarkFolderEditor,
dropdownMenu: dropdownMenuReducer,
timelines,
meta,
M app/javascript/flavours/glitch/reducers/status_lists.js => app/javascript/flavours/glitch/reducers/status_lists.js +79 -49
@@ 96,56 96,86 @@ const removeOneFromList = (state, listType, status) => {
return state.updateIn([listType, "items"], (list) => list.delete(status.get("id")));
};
+const addBookmark = (state, status, folderId) => {
+ state = prependOneToList(state, 'bookmarks', status);
+ return state.map((list, key) => {
+ if (!key.startsWith('bookmarks:')) return list;
+ return list.update('items', (list) => {
+ if (key === `bookmarks:${folderId}`) {
+ if (list.includes(status.get('id'))) {
+ return list;
+ } else {
+ return ImmutableOrderedSet([status.get('id')]).union(list);
+ }
+ } else {
+ return list.delete(status.get('id'));
+ }
+ });
+ });
+};
+
+const removeBookmark = (state, status) => {
+ return state.map((list, key) => {
+ if (!key.startsWith('bookmarks')) return list;
+ return list.update('items', (list) => list.delete(status.get('id')));
+ });
+};
+
export default function statusLists(state = initialState, action) {
+ let key;
switch(action.type) {
- case FAVOURITED_STATUSES_FETCH_REQUEST:
- case FAVOURITED_STATUSES_EXPAND_REQUEST:
- return state.setIn(["favourites", "isLoading"], true);
- case FAVOURITED_STATUSES_FETCH_FAIL:
- case FAVOURITED_STATUSES_EXPAND_FAIL:
- return state.setIn(["favourites", "isLoading"], false);
- case FAVOURITED_STATUSES_FETCH_SUCCESS:
- return normalizeList(state, "favourites", action.statuses, action.next);
- case FAVOURITED_STATUSES_EXPAND_SUCCESS:
- return appendToList(state, "favourites", action.statuses, action.next);
- case BOOKMARKED_STATUSES_FETCH_REQUEST:
- case BOOKMARKED_STATUSES_EXPAND_REQUEST:
- return state.setIn(["bookmarks", "isLoading"], true);
- case BOOKMARKED_STATUSES_FETCH_FAIL:
- case BOOKMARKED_STATUSES_EXPAND_FAIL:
- return state.setIn(["bookmarks", "isLoading"], false);
- case BOOKMARKED_STATUSES_FETCH_SUCCESS:
- return normalizeList(state, "bookmarks", action.statuses, action.next);
- case BOOKMARKED_STATUSES_EXPAND_SUCCESS:
- return appendToList(state, "bookmarks", action.statuses, action.next);
- case TRENDS_STATUSES_FETCH_REQUEST:
- case TRENDS_STATUSES_EXPAND_REQUEST:
- return state.setIn(["trending", "isLoading"], true);
- case TRENDS_STATUSES_FETCH_FAIL:
- case TRENDS_STATUSES_EXPAND_FAIL:
- return state.setIn(["trending", "isLoading"], false);
- case TRENDS_STATUSES_FETCH_SUCCESS:
- return normalizeList(state, "trending", action.statuses, action.next);
- case TRENDS_STATUSES_EXPAND_SUCCESS:
- return appendToList(state, "trending", action.statuses, action.next);
- case FAVOURITE_SUCCESS:
- return prependOneToList(state, "favourites", action.status);
- case UNFAVOURITE_SUCCESS:
- return removeOneFromList(state, "favourites", action.status);
- case BOOKMARK_SUCCESS:
- return prependOneToList(state, "bookmarks", action.status);
- case UNBOOKMARK_SUCCESS:
- return removeOneFromList(state, "bookmarks", action.status);
- case PINNED_STATUSES_FETCH_SUCCESS:
- return normalizeList(state, "pins", action.statuses, action.next);
- case PIN_SUCCESS:
- return prependOneToList(state, "pins", action.status);
- case UNPIN_SUCCESS:
- return removeOneFromList(state, "pins", action.status);
- case ACCOUNT_BLOCK_SUCCESS:
- case ACCOUNT_MUTE_SUCCESS:
- return state.updateIn(["trending", "items"], ImmutableOrderedSet(), list => list.filterNot(statusId => action.statuses.getIn([statusId, "account"]) === action.relationship.id));
- default:
- return state;
+ case FAVOURITED_STATUSES_FETCH_REQUEST:
+ case FAVOURITED_STATUSES_EXPAND_REQUEST:
+ return state.setIn(["favourites", "isLoading"], true);
+ case FAVOURITED_STATUSES_FETCH_FAIL:
+ case FAVOURITED_STATUSES_EXPAND_FAIL:
+ return state.setIn(["favourites", "isLoading"], false);
+ case FAVOURITED_STATUSES_FETCH_SUCCESS:
+ return normalizeList(state, "favourites", action.statuses, action.next);
+ case FAVOURITED_STATUSES_EXPAND_SUCCESS:
+ return appendToList(state, "favourites", action.statuses, action.next);
+ case BOOKMARKED_STATUSES_FETCH_REQUEST:
+ case BOOKMARKED_STATUSES_EXPAND_REQUEST:
+ key = action.folderId ? `bookmarks:${action.folderId}`: "bookmarks";
+ return state.setIn([key, "isLoading"], true);
+ case BOOKMARKED_STATUSES_FETCH_FAIL:
+ case BOOKMARKED_STATUSES_EXPAND_FAIL:
+ key = action.folderId ? `bookmarks:${action.folderId}`: "bookmarks";
+ return state.setIn([key, "isLoading"], false);
+ case BOOKMARKED_STATUSES_FETCH_SUCCESS:
+ key = action.folderId ? `bookmarks:${action.folderId}`: "bookmarks";
+ return normalizeList(state, key, action.statuses, action.next);
+ case BOOKMARKED_STATUSES_EXPAND_SUCCESS:
+ key = action.folderId ? `bookmarks:${action.folderId}`: "bookmarks";
+ return appendToList(state, key, action.statuses, action.next);
+ case TRENDS_STATUSES_FETCH_REQUEST:
+ case TRENDS_STATUSES_EXPAND_REQUEST:
+ return state.setIn(["trending", "isLoading"], true);
+ case TRENDS_STATUSES_FETCH_FAIL:
+ case TRENDS_STATUSES_EXPAND_FAIL:
+ return state.setIn(["trending", "isLoading"], false);
+ case TRENDS_STATUSES_FETCH_SUCCESS:
+ return normalizeList(state, "trending", action.statuses, action.next);
+ case TRENDS_STATUSES_EXPAND_SUCCESS:
+ return appendToList(state, "trending", action.statuses, action.next);
+ case FAVOURITE_SUCCESS:
+ return prependOneToList(state, "favourites", action.status);
+ case UNFAVOURITE_SUCCESS:
+ return removeOneFromList(state, "favourites", action.status);
+ case BOOKMARK_SUCCESS:
+ return addBookmark(state, action.status, action.folderId);
+ case UNBOOKMARK_SUCCESS:
+ return removeBookmark(state, action.status);
+ case PINNED_STATUSES_FETCH_SUCCESS:
+ return normalizeList(state, "pins", action.statuses, action.next);
+ case PIN_SUCCESS:
+ return prependOneToList(state, "pins", action.status);
+ case UNPIN_SUCCESS:
+ return removeOneFromList(state, "pins", action.status);
+ case ACCOUNT_BLOCK_SUCCESS:
+ case ACCOUNT_MUTE_SUCCESS:
+ return state.updateIn(["trending", "items"], ImmutableOrderedSet(), list => list.filterNot(statusId => action.statuses.getIn([statusId, "account"]) === action.relationship.id));
+ default:
+ return state;
}
}
M app/javascript/flavours/glitch/selectors/index.js => app/javascript/flavours/glitch/selectors/index.js +11 -21
@@ 1,4 1,4 @@
-import { List as ImmutableList, Map as ImmutableMap } from "immutable";
+import { List as ImmutableList, Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from "immutable";
import { createSelector } from "reselect";
import { me } from "flavours/glitch/initial_state";
@@ 86,26 86,16 @@ export const makeGetPictureInPicture = () => {
}));
};
-const getAlertsBase = state => state.get("alerts");
-
-export const getAlerts = createSelector([getAlertsBase], (base) => {
- let arr = [];
-
- base.forEach(item => {
- arr.push({
- message: item.get("message"),
- message_values: item.get("message_values"),
- title: item.get("title"),
- key: item.get("key"),
- dismissAfter: 5000,
- barStyle: {
- zIndex: 200,
- },
- });
- });
+const ALERT_DEFAULTS = {
+ dismissAfter: 5000,
+ style: false,
+};
- return arr;
-});
+export const getAlerts = createSelector(state => state.get("alerts"), alerts =>
+ alerts.map(item => ({
+ ...ALERT_DEFAULTS,
+ ...item,
+ })).toArray());
export const makeGetNotification = () => createSelector([
(_, base) => base,
@@ 141,5 131,5 @@ export const getAccountHidden = createSelector([
});
export const getStatusList = createSelector([
- (state, type) => state.getIn(["status_lists", type, "items"]),
+ (state, type) => state.getIn(["status_lists", type, "items"], ImmutableOrderedSet()),
], (items) => items.toList());
A app/javascript/flavours/glitch/styles/components/bookmark_folders.scss => app/javascript/flavours/glitch/styles/components/bookmark_folders.scss +12 -0
@@ 0,0 1,12 @@
+.select-bookmark-folder {
+ background: $ui-base-color;
+ flex-direction: column;
+ border-radius: 8px;
+ box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
+ width: 380px;
+ overflow: hidden;
+
+ @media screen and (width <= 420px) {
+ width: 90%;
+ }
+}
M app/javascript/flavours/glitch/styles/components/index.scss => app/javascript/flavours/glitch/styles/components/index.scss +1 -0
@@ 24,3 24,4 @@
@import 'signed_out';
@import 'privacy_policy';
@import 'about';
+@import 'bookmark_folders';
M app/javascript/flavours/glitch/styles/components/misc.scss => app/javascript/flavours/glitch/styles/components/misc.scss +59 -0
@@ 1614,6 1614,65 @@ button.icon-button.active i.fa-retweet {
min-width: 75%;
}
+.notification-list {
+ position: fixed;
+ bottom: 2rem;
+ inset-inline-start: 0;
+ z-index: 999;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.notification-bar {
+ flex: 0 0 auto;
+ position: relative;
+ inset-inline-start: -100%;
+ width: auto;
+ padding: 15px;
+ margin: 0;
+ color: $white;
+ background: rgba($black, 0.85);
+ backdrop-filter: blur(8px);
+ border: 1px solid rgba(lighten($classic-base-color, 4%), 0.85);
+ border-radius: 8px;
+ box-shadow: 0 10px 15px -3px rgba($base-shadow-color, 0.25),
+ 0 4px 6px -4px rgba($base-shadow-color, 0.25);
+ cursor: default;
+ transition: 0.5s cubic-bezier(0.89, 0.01, 0.5, 1.1);
+ transform: translateZ(0);
+ font-size: 15px;
+ line-height: 21px;
+
+ &.notification-bar-active {
+ inset-inline-start: 1rem;
+ }
+}
+
+.notification-bar-title {
+ margin-inline-end: 5px;
+}
+
+.notification-bar-title,
+.notification-bar-action {
+ font-weight: 700;
+}
+
+.notification-bar-action {
+ text-transform: uppercase;
+ margin-inline-start: 10px;
+ cursor: pointer;
+ color: $blurple-300;
+ border-radius: 4px;
+ padding: 0 4px;
+
+ &:hover,
+ &:focus,
+ &:active {
+ background: rgba($ui-base-color, 0.85);
+ }
+}
+
::-webkit-scrollbar-thumb {
border-radius: 0;
}