Reaches Meteor 2.3 building + running against Mongo 3.2, REST smoke test
byte-identical to the 1.6.1.1 baseline (all 12 Flutter endpoints green).
Dead/abandoned Atmosphere packages removed or replaced (the upgrade blockers):
- arkham:comments-ui (no Meteor 2.x build; pinned accounts-password@1.x):
reimplemented the fire-page comments as a React feature:
imports/api/Comments/ (collection, server methods, publication, media
analyzers, new-fire-comment email) + imports/ui/components/Comments/CommentsBox.
Comment text now rendered as safe plain text + image/youtube embed (was
markdown-to-HTML). Wired into Fires.js; sitemaps.js + migration v11 updated
to the new collection. Old Blaze/startup comments files deleted.
- nimble:restivus (REST API; pinned accounts-password@1.3.3, CoffeeScript source
that crashes this build host): vendored as a local package
packages/nimble-restivus with the .coffee precompiled to plain JS and the
accounts-password constraint loosened to 2.x. REST behavior unchanged
(verified by smoke test). This also dropped the entire iron:router stack.
- maximum:server-transform (+ peerlibrary:*, meteorhacks:zones/inject-initial):
unused; removal broke Meteor.publishTransformed in FalsePositives publications
-> replaced with plain Meteor.publish (no transform was configured).
- less, markdown (no source files), and the dead test stack
(meteortesting:mocha, practicalmeteor:chai, xolvio:cleaner) which transitively
pulled coffeescript@1.0.17 — the build plugin that crashed meteor-tool
(node_contextify assertion) on this machine. Removing it unblocked the build.
- fourseven:scss 4.5.4 -> 4.14.1 (node-sass 4.5.3 doesn't build on node 12).
npm-mongo driver at 2.3 is 3.9.x — still compatible with the production Mongo
3.2 replica set.
152 lines
5.7 KiB
JavaScript
152 lines
5.7 KiB
JavaScript
/* eslint-disable import/no-absolute-path */
|
|
/* eslint-disable jsx-a11y/media-has-caption */
|
|
import React from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import { withTracker } from 'meteor/react-meteor-data';
|
|
import { translate } from 'react-i18next';
|
|
import { Meteor } from 'meteor/meteor';
|
|
import moment from 'moment';
|
|
import CommentsCollection from '/imports/api/Comments/Comments';
|
|
import './Comments.scss';
|
|
|
|
function MediaEmbed({ media }) {
|
|
if (!media || !media.content) return null;
|
|
if (media.type === 'image') {
|
|
return <img className="comment-media-image" src={media.content} alt="" />;
|
|
}
|
|
if (media.type === 'youtube') {
|
|
return (
|
|
<div className="comment-media-youtube">
|
|
<iframe title="youtube" src={media.content} frameBorder="0" allowFullScreen />
|
|
</div>
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
MediaEmbed.propTypes = { media: PropTypes.object };
|
|
MediaEmbed.defaultProps = { media: null };
|
|
|
|
class CommentsBox extends React.Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = { draft: '', editingId: null, editDraft: '' };
|
|
}
|
|
|
|
call(method, ...args) {
|
|
Meteor.call(method, ...args, (err) => {
|
|
if (err) console.warn(`${method}: ${err.reason || err.message}`);
|
|
});
|
|
}
|
|
|
|
submit = (e) => {
|
|
e.preventDefault();
|
|
const content = this.state.draft.trim();
|
|
if (!content) return;
|
|
this.setState({ draft: '' });
|
|
this.call('comments.insert', this.props.referenceId, content);
|
|
};
|
|
|
|
saveEdit = (id) => {
|
|
const content = this.state.editDraft.trim();
|
|
if (!content) return;
|
|
this.setState({ editingId: null, editDraft: '' });
|
|
this.call('comments.edit', id, content);
|
|
};
|
|
|
|
renderComment(c) {
|
|
const { t } = this.props;
|
|
const currentUserId = Meteor.userId();
|
|
const isOwner = currentUserId && c.userId === currentUserId;
|
|
const likes = (c.likes || []).length;
|
|
const dislikes = (c.dislikes || []).length;
|
|
const liked = currentUserId && (c.likes || []).includes(currentUserId);
|
|
const disliked = currentUserId && (c.dislikes || []).includes(currentUserId);
|
|
const author = c.username || t('Anónimo');
|
|
return (
|
|
<li key={c._id} className="comment">
|
|
<div className="comment-header">
|
|
<span className="comment-author">{author}</span>
|
|
<span className="comment-date">{moment(c.createdAt).format('LLL')}</span>
|
|
</div>
|
|
{this.state.editingId === c._id ? (
|
|
<div className="comment-edit">
|
|
<textarea
|
|
className="form-control"
|
|
value={this.state.editDraft}
|
|
onChange={ev => this.setState({ editDraft: ev.target.value })}
|
|
/>
|
|
<button type="button" className="btn btn-primary btn-sm" onClick={() => this.saveEdit(c._id)}>{t('Guardar')}</button>
|
|
<button type="button" className="btn btn-link btn-sm" onClick={() => this.setState({ editingId: null })}>{t('Borrar')}</button>
|
|
</div>
|
|
) : (
|
|
// Plain text (whitespace preserved via CSS). Rendered as text — not
|
|
// HTML — so user content can never inject markup (safer than the old
|
|
// markdown-to-HTML comments package).
|
|
<p className="comment-content">{c.content}</p>
|
|
)}
|
|
<MediaEmbed media={c.media} />
|
|
<div className="comment-actions">
|
|
<button type="button" className={`btn btn-link btn-sm${liked ? ' active' : ''}`} disabled={!currentUserId} onClick={() => this.call('comments.like', c._id)}>
|
|
{'👍'} {likes}
|
|
</button>
|
|
<button type="button" className={`btn btn-link btn-sm${disliked ? ' active' : ''}`} disabled={!currentUserId} onClick={() => this.call('comments.dislike', c._id)}>
|
|
{'👎'} {dislikes}
|
|
</button>
|
|
{isOwner && (
|
|
<span className="comment-owner-actions">
|
|
<button type="button" className="btn btn-link btn-sm" onClick={() => this.setState({ editingId: c._id, editDraft: c.content })}>{t('Editar')}</button>
|
|
<button type="button" className="btn btn-link btn-sm remove-action" onClick={() => this.call('comments.remove', c._id)}>{t('Borrar')}</button>
|
|
</span>
|
|
)}
|
|
</div>
|
|
</li>
|
|
);
|
|
}
|
|
|
|
render() {
|
|
const { t, comments, ready } = this.props;
|
|
const loggedIn = !!Meteor.userId();
|
|
return (
|
|
<div className="comments-box">
|
|
<ul className="comments-list">
|
|
{comments.map(c => this.renderComment(c))}
|
|
</ul>
|
|
{ready && comments.length === 0 && (
|
|
<p className="comments-empty">{t('Añadir un comentario')}</p>
|
|
)}
|
|
{loggedIn ? (
|
|
<form className="comment-form" onSubmit={this.submit}>
|
|
<textarea
|
|
className="form-control create-comment"
|
|
placeholder={t('Añadir un comentario')}
|
|
value={this.state.draft}
|
|
onChange={ev => this.setState({ draft: ev.target.value })}
|
|
/>
|
|
<button type="submit" className="btn btn-primary">{t('Añadir comentario')}</button>
|
|
</form>
|
|
) : (
|
|
<p className="comment-login-hint">
|
|
{t('Necesitas iniciar sesión para')} {t('añadir comentarios')}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
CommentsBox.propTypes = {
|
|
t: PropTypes.func.isRequired,
|
|
referenceId: PropTypes.string.isRequired,
|
|
comments: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
ready: PropTypes.bool.isRequired
|
|
};
|
|
|
|
const CommentsBoxContainer = withTracker(({ referenceId }) => {
|
|
const handle = Meteor.subscribe('comments.forReference', referenceId);
|
|
return {
|
|
ready: handle.ready(),
|
|
comments: CommentsCollection.find({ referenceId }, { sort: { createdAt: 1 } }).fetch()
|
|
};
|
|
})(CommentsBox);
|
|
|
|
export default translate([], { wait: true })(CommentsBoxContainer);
|