revision history ui

This commit is contained in:
2458773093 2019-09-17 18:23:51 +03:00
parent 7219c9b5e6
commit 498630f20f
15 changed files with 257 additions and 10 deletions

View File

@ -6,7 +6,7 @@ class Api::V1::StatusesController < Api::BaseController
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy]
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy]
before_action :require_user!, except: [:show, :context, :card]
before_action :set_status, only: [:show, :context, :card, :update]
before_action :set_status, only: [:show, :context, :card, :update, :revisions]
respond_to :json
@ -33,14 +33,10 @@ class Api::V1::StatusesController < Api::BaseController
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
end
def card
@card = @status.preview_cards.first
def revisions
@revisions = @status.revisions
if @card.nil?
render_empty
else
render json: @card, serializer: REST::PreviewCardSerializer
end
render json: @revisions, each_serializer: REST::StatusRevisionSerializer
end
def create

View File

@ -0,0 +1,16 @@
import api from '../api';
export const STATUS_REVISION_LIST_LOAD = 'STATUS_REVISION_LIST';
export const STATUS_REVISION_LIST_LOAD_SUCCESS = 'STATUS_REVISION_LIST_SUCCESS';
export const STATUS_REVISION_LIST_LOAD_FAIL = 'STATUS_REVISION_LIST_FAIL';
const loadSuccess = data => ({ type: STATUS_REVISION_LIST_LOAD_SUCCESS, payload: data });
const loadFail = e => ({ type: STATUS_REVISION_LIST_LOAD_FAIL, payload: e });
export function load(statusId) {
return (dispatch, getState) => {
api(getState).get(`/api/v1/statuses/${statusId}/revisions`)
.then(res => dispatch(loadSuccess(res.data)))
.catch(e => dispatch(loadFail(e)));
};
}

View File

@ -67,6 +67,7 @@ class Status extends ImmutablePureComponent {
otherAccounts: ImmutablePropTypes.list,
onClick: PropTypes.func,
onReply: PropTypes.func,
onShowRevisions: PropTypes.func,
onQuote: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
@ -438,9 +439,10 @@ class Status extends ImmutablePureComponent {
</NavLink>
</div>
{!group && status.get('group') && (
{((!group && status.get('group')) || status.get('revised_at') !== null) && (
<div className='status__meta'>
Posted in <NavLink to={`/groups/${status.getIn(['group', 'id'])}`}>{status.getIn(['group', 'title'])}</NavLink>
{!group && status.get('group') && <React.Fragment>Posted in <NavLink to={`/groups/${status.getIn(['group', 'id'])}`}>{status.getIn(['group', 'title'])}</NavLink></React.Fragment>}
{status.get('revised_at') !== null && <a onClick={() => other.onShowRevisions(status)}> Edited</a>}
</div>
)}

View File

@ -105,6 +105,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
onShowRevisions (status) {
dispatch(openModal('STATUS_REVISION', { status }));
},
onFavourite (status) {
if (status.get('favourited')) {
dispatch(unfavourite(status));

View File

@ -20,6 +20,7 @@ import {
EmbedModal,
ListEditor,
ListAdder,
StatusRevisionModal,
} from '../../../features/ui/util/async-components';
const MODAL_COMPONENTS = {
@ -35,6 +36,7 @@ const MODAL_COMPONENTS = {
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
'LIST_ADDER':ListAdder,
'HOTKEYS': () => Promise.resolve({ default: HotkeysModal }),
'STATUS_REVISION': StatusRevisionModal,
'COMPOSE': () => Promise.resolve({ default: ComposeModal }),
'UNAUTHORIZED': () => Promise.resolve({ default: UnauthorizedModal }),
};

View File

@ -0,0 +1,44 @@
import React from 'react';
import { injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ModalLoading from './modal_loading';
import RelativeTimestamp from '../../../components/relative_timestamp';
export default @injectIntl
class StatusRevisionsList extends ImmutablePureComponent {
static propTypes = {
loading: PropTypes.bool.isRequired,
error: PropTypes.object,
data: PropTypes.array
};
render () {
const { loading, error, data } = this.props;
if (loading || !data) return <ModalLoading />;
if (error) return (
<div className='status-revisions-list'>
<div className='status-revisions-list__error'>
An error occured
</div>
</div>
);
return (
<div className='status-revisions-list'>
{data.map((revision, i) => (
<div key={i} className='status-revisions-list__item'>
<div className='status-revisions-list__item__timestamp'>
<RelativeTimestamp timestamp={revision.created_at} />
</div>
<div className='status-revisions-list__item__text'>{revision.text}</div>
</div>
))}
</div>
);
}
}

View File

@ -0,0 +1,40 @@
import React from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import IconButton from 'gabsocial/components/icon_button';
import StatusRevisionListContainer from '../containers/status_revision_list_container';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
export default @injectIntl
class StatusRevisionModal extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
status: ImmutablePropTypes.map.isRequired
};
render () {
const { intl, onClose, status } = this.props;
return (
<div className='modal-root__modal'>
<div className='status-revisions'>
<div className='status-revisions__header'>
<h3 className='status-revisions__header__title'><FormattedMessage id='status_revisions.heading' defaultMessage='Revision History' /></h3>
<IconButton className='status-revisions__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={20} />
</div>
<div className='status-revisions__content'>
<StatusRevisionListContainer id={status.get('id')} />
</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,27 @@
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { load } from '../../../actions/status_revision_list';
import StatusRevisionList from '../components/status_revision_list';
class StatusRevisionListContainer extends ImmutablePureComponent {
componentDidMount() {
this.props.load(this.props.id);
}
render() {
return <StatusRevisionList {...this.props} />;
}
}
const mapStateToProps = state => ({
loading: state.getIn(['status_revision_list', 'loading']),
error: state.getIn(['status_revision_list', 'error']),
data: state.getIn(['status_revision_list', 'data']),
});
const mapDispatchToProps = {
load
};
export default connect(mapStateToProps, mapDispatchToProps)(StatusRevisionListContainer);

View File

@ -122,6 +122,10 @@ export function MuteModal () {
return import(/* webpackChunkName: "modals/mute_modal" */'../components/mute_modal');
}
export function StatusRevisionModal () {
return import(/* webpackChunkName: "modals/mute_modal" */'../components/status_revision_modal');
}
export function ReportModal () {
return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal');
}

View File

@ -37,6 +37,7 @@ import group_relationships from './group_relationships';
import group_lists from './group_lists';
import group_editor from './group_editor';
import sidebar from './sidebar';
import status_revision_list from './status_revision_list';
const reducers = {
dropdown_menu,
@ -77,6 +78,7 @@ const reducers = {
group_lists,
group_editor,
sidebar,
status_revision_list,
};
export default combineReducers(reducers);

View File

@ -0,0 +1,31 @@
import { Map as ImmutableMap } from 'immutable';
import {
STATUS_REVISION_LIST_LOAD,
STATUS_REVISION_LIST_LOAD_SUCCESS,
STATUS_REVISION_LIST_LOAD_FAIL
} from '../actions/status_revision_list';
const initialState = ImmutableMap({
loading: false,
error: null,
data: null
});
export default function statusRevisionList(state = initialState, action) {
switch(action.type) {
case STATUS_REVISION_LIST_LOAD:
return initialState;
case STATUS_REVISION_LIST_LOAD_SUCCESS:
return state.withMutations(mutable => {
mutable.set('loading', false);
mutable.set('data', action.payload);
});
case STATUS_REVISION_LIST_LOAD_FAIL:
return state.withMutations(mutable => {
mutable.set('loading', false);
mutable.set('error', action.payload);
});
default:
return state;
}
};

View File

@ -32,6 +32,7 @@
@import 'gabsocial/components/group-form';
@import 'gabsocial/components/group-sidebar-panel';
@import 'gabsocial/components/sidebar-menu';
@import 'gabsocial/components/status-revisions';
@import 'gabsocial/polls';
@import 'gabsocial/introduction';

View File

@ -0,0 +1,72 @@
.status-revisions {
padding: 8px 0 0;
overflow: hidden;
background-color: $classic-base-color;
border-radius: 6px;
@media screen and (max-width: 960px) {
height: 90vh;
}
&__header {
display: block;
position: relative;
border-bottom: 1px solid lighten($classic-base-color, 8%);
border-radius: 6px 6px 0 0;
padding-top: 12px;
padding-bottom: 12px;
&__title {
display: block;
width: 80%;
margin: 0 auto;
font-size: 18px;
font-weight: bold;
line-height: 24px;
color: $primary-text-color;
text-align: center;
}
}
&__close {
position: absolute;
right: 10px;
top: 10px;
}
&__content {
display: flex;
flex-direction: row;
width: 500px;
flex-direction: column;
overflow: hidden;
overflow-y: scroll;
height: calc(100% - 80px);
-webkit-overflow-scrolling: touch;
widows: 90%;
}
&-list {
width: 100%;
&__error {
padding: 15px;
text-align: center;
font-weight: bold;
}
&__item {
padding: 15px;
border-bottom: 1px solid lighten($classic-base-color, 8%);
&__timestamp {
opacity: 0.5;
font-size: 13px;
}
&__text {
font-size: 15px;
}
}
}
}

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
class REST::StatusRevisionSerializer < ActiveModel::Serializer
attributes :created_at, :text
end

View File

@ -305,6 +305,7 @@ Rails.application.routes.draw do
member do
get :context
get :card
get :revisions
end
end