escala 2+3: Meteor 1.8.3 -> 2.3 (Mongo 3.2 compatible), dead-package remediation
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.
This commit is contained in:
parent
ccfc80547a
commit
cd570b9d9c
24 changed files with 1638 additions and 312 deletions
|
|
@ -4,33 +4,33 @@
|
||||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||||
# but you can also edit it by hand.
|
# but you can also edit it by hand.
|
||||||
|
|
||||||
meteor-base@1.4.0 # Packages every Meteor app needs to have
|
meteor-base@1.5.0 # Packages every Meteor app needs to have
|
||||||
mobile-experience@1.0.5 # Packages for a great mobile UX
|
mobile-experience@1.1.0 # Packages for a great mobile UX
|
||||||
mongo@1.7.0 # The database Meteor supports right now
|
mongo@1.12.0 # The database Meteor supports right now
|
||||||
reactive-var@1.0.11 # Reactive variable for tracker
|
reactive-var@1.0.11 # Reactive variable for tracker
|
||||||
tracker@1.2.0 # Meteor's client-side reactive programming library
|
tracker@1.2.0 # Meteor's client-side reactive programming library
|
||||||
|
|
||||||
standard-minifier-css@1.5.4 # CSS minifier run for production mode
|
standard-minifier-css@1.7.2 # CSS minifier run for production mode
|
||||||
standard-minifier-js@2.5.2 # JS minifier run for production mode
|
standard-minifier-js@2.6.0 # JS minifier run for production mode
|
||||||
es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers.
|
es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers.
|
||||||
ecmascript@0.13.2 # Enable ECMAScript2015+ syntax in app code
|
ecmascript@0.15.2 # Enable ECMAScript2015+ syntax in app code
|
||||||
shell-server@0.4.0 # Server-side component of the `meteor shell` command
|
shell-server@0.5.0 # Server-side component of the `meteor shell` command
|
||||||
|
|
||||||
react-meteor-data
|
react-meteor-data
|
||||||
alanning:roles
|
alanning:roles
|
||||||
fourseven:scss
|
fourseven:scss@4.14.1
|
||||||
accounts-base@1.4.5
|
accounts-base@2.0.0
|
||||||
accounts-password@1.5.2
|
accounts-password@2.0.0
|
||||||
service-configuration@1.0.11
|
service-configuration@1.1.0
|
||||||
accounts-facebook@1.3.2
|
accounts-facebook@1.3.3
|
||||||
accounts-github@1.4.2
|
accounts-github@1.5.0
|
||||||
accounts-google@1.3.3
|
accounts-google@1.4.0
|
||||||
themeteorchef:bert
|
themeteorchef:bert
|
||||||
fortawesome:fontawesome
|
fortawesome:fontawesome
|
||||||
aldeed:collection2-core@2.0.1
|
aldeed:collection2-core@2.0.1
|
||||||
audit-argument-checks@1.0.7
|
audit-argument-checks@1.0.7
|
||||||
ddp-rate-limiter@1.0.7
|
ddp-rate-limiter@1.1.0
|
||||||
dynamic-import@0.5.1
|
dynamic-import@0.7.0
|
||||||
static-html
|
static-html
|
||||||
alexwine:bootstrap-4
|
alexwine:bootstrap-4
|
||||||
|
|
||||||
|
|
@ -42,29 +42,19 @@ flowkey:raven # js errors
|
||||||
vjrj:piwik # Stats
|
vjrj:piwik # Stats
|
||||||
mdg:geolocation
|
mdg:geolocation
|
||||||
natestrauser:publish-performant-counts
|
natestrauser:publish-performant-counts
|
||||||
maximum:server-transform
|
|
||||||
mys:fonts # fonst in npm
|
mys:fonts # fonst in npm
|
||||||
ostrio:mailer # mailer
|
ostrio:mailer # mailer
|
||||||
# babrahams:constellation # dev utility
|
# babrahams:constellation # dev utility
|
||||||
percolate:migrations # db migrations
|
percolate:migrations # db migrations
|
||||||
ostrio:meteor-root
|
ostrio:meteor-root
|
||||||
meteortesting:mocha
|
|
||||||
practicalmeteor:chai
|
|
||||||
aldeed:schema-deny
|
aldeed:schema-deny
|
||||||
aldeed:template-extension
|
|
||||||
dburles:collection-helpers
|
dburles:collection-helpers
|
||||||
less@2.7.11
|
|
||||||
markdown@1.0.12
|
|
||||||
mongo-livedata@1.0.12
|
mongo-livedata@1.0.12
|
||||||
reywood:publish-composite
|
reywood:publish-composite
|
||||||
barbatus:stars-rating
|
|
||||||
arkham:comments-ui
|
|
||||||
facts@1.0.9
|
facts@1.0.9
|
||||||
gadicohen:sitemaps
|
gadicohen:sitemaps
|
||||||
nspangler:autoreconnect
|
nspangler:autoreconnect
|
||||||
saucecode:timezoned-synced-cron
|
saucecode:timezoned-synced-cron
|
||||||
# lmachens:kadira
|
# lmachens:kadira
|
||||||
meteorhacks:zones
|
nimble:restivus@0.8.12
|
||||||
nimble:restivus
|
underscore@1.0.10
|
||||||
xolvio:cleaner
|
|
||||||
underscore
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
METEOR@1.8.3
|
METEOR@2.3
|
||||||
|
|
|
||||||
169
.meteor/versions
169
.meteor/versions
|
|
@ -1,157 +1,128 @@
|
||||||
255kb:meteor-status@1.5.0
|
255kb:meteor-status@1.5.0
|
||||||
accounts-base@1.4.5
|
accounts-base@2.0.1
|
||||||
accounts-facebook@1.3.2
|
accounts-facebook@1.3.3
|
||||||
accounts-github@1.4.2
|
accounts-github@1.5.0
|
||||||
accounts-google@1.3.3
|
accounts-google@1.4.0
|
||||||
accounts-oauth@1.1.16
|
accounts-oauth@1.3.0
|
||||||
accounts-password@1.5.2
|
accounts-password@2.0.1
|
||||||
alanning:roles@1.2.16
|
alanning:roles@1.3.0
|
||||||
aldeed:collection2-core@2.1.2
|
aldeed:collection2-core@2.1.2
|
||||||
aldeed:schema-deny@2.0.1
|
aldeed:schema-deny@2.0.1
|
||||||
aldeed:template-extension@4.1.0
|
|
||||||
alexwine:bootstrap-4@4.1.0
|
alexwine:bootstrap-4@4.1.0
|
||||||
allow-deny@1.1.0
|
allow-deny@1.1.0
|
||||||
arkham:comments-ui@1.4.4
|
|
||||||
audit-argument-checks@1.0.7
|
audit-argument-checks@1.0.7
|
||||||
autoupdate@1.6.0
|
autoupdate@1.7.0
|
||||||
babel-compiler@7.4.2
|
babel-compiler@7.6.2
|
||||||
babel-runtime@1.4.0
|
babel-runtime@1.5.0
|
||||||
barbatus:stars-rating@1.1.1
|
|
||||||
base64@1.0.12
|
base64@1.0.12
|
||||||
binary-heap@1.0.11
|
binary-heap@1.0.11
|
||||||
blaze@2.3.3
|
blaze@2.5.0
|
||||||
blaze-tools@1.0.10
|
blaze-tools@1.1.0
|
||||||
boilerplate-generator@1.6.0
|
boilerplate-generator@1.7.1
|
||||||
browser-policy@1.1.0
|
caching-compiler@1.2.2
|
||||||
browser-policy-common@1.0.11
|
caching-html-compiler@1.2.0
|
||||||
browser-policy-content@1.1.0
|
callback-hook@1.3.0
|
||||||
browser-policy-framing@1.1.0
|
|
||||||
caching-compiler@1.2.1
|
|
||||||
caching-html-compiler@1.1.2
|
|
||||||
callback-hook@1.2.0
|
|
||||||
check@1.3.1
|
check@1.3.1
|
||||||
coffeescript@1.0.17
|
|
||||||
dburles:collection-helpers@1.1.0
|
dburles:collection-helpers@1.1.0
|
||||||
dburles:mongo-collection-instances@0.3.5
|
|
||||||
ddp@1.4.0
|
ddp@1.4.0
|
||||||
ddp-client@2.3.3
|
ddp-client@2.5.0
|
||||||
ddp-common@1.4.0
|
ddp-common@1.4.0
|
||||||
ddp-rate-limiter@1.0.7
|
ddp-rate-limiter@1.1.0
|
||||||
ddp-server@2.3.0
|
ddp-server@2.4.0
|
||||||
deps@1.0.12
|
|
||||||
diff-sequence@1.1.1
|
diff-sequence@1.1.1
|
||||||
dynamic-import@0.5.1
|
dynamic-import@0.7.0
|
||||||
ecmascript@0.13.2
|
ecmascript@0.15.2
|
||||||
ecmascript-runtime@0.7.0
|
ecmascript-runtime@0.7.0
|
||||||
ecmascript-runtime-client@0.9.0
|
ecmascript-runtime-client@0.11.1
|
||||||
ecmascript-runtime-server@0.8.0
|
ecmascript-runtime-server@0.10.1
|
||||||
ejson@1.1.0
|
ejson@1.1.1
|
||||||
email@1.2.3
|
email@2.1.0
|
||||||
es5-shim@4.8.0
|
es5-shim@4.8.0
|
||||||
facebook-oauth@1.6.0
|
facebook-oauth@1.9.0
|
||||||
facts@1.0.9
|
facts@1.0.9
|
||||||
fetch@0.1.1
|
fetch@0.1.1
|
||||||
flowkey:raven@1.1.0
|
flowkey:raven@1.1.0
|
||||||
fortawesome:fontawesome@4.7.0
|
fortawesome:fontawesome@4.7.0
|
||||||
fourseven:scss@4.5.4
|
fourseven:scss@4.14.1
|
||||||
gadicc:blaze-react-component@1.4.0
|
gadicc:blaze-react-component@1.4.0
|
||||||
gadicohen:robots-txt@0.0.10
|
gadicohen:robots-txt@0.0.10
|
||||||
gadicohen:sitemaps@0.0.26
|
gadicohen:sitemaps@0.0.26
|
||||||
geojson-utils@1.0.10
|
geojson-utils@1.0.10
|
||||||
github-oauth@1.2.2
|
github-oauth@1.3.0
|
||||||
google-oauth@1.2.6
|
google-oauth@1.4.0
|
||||||
hot-code-push@1.0.4
|
hot-code-push@1.0.4
|
||||||
html-tools@1.0.11
|
html-tools@1.1.0
|
||||||
htmljs@1.0.11
|
htmljs@1.1.0
|
||||||
http@1.4.2
|
http@2.0.0
|
||||||
id-map@1.1.0
|
id-map@1.1.1
|
||||||
inter-process-messaging@0.1.0
|
inter-process-messaging@0.1.1
|
||||||
jquery@1.11.11
|
jquery@1.11.11
|
||||||
lai:collection-extensions@0.2.1_1
|
launch-screen@1.3.0
|
||||||
launch-screen@1.1.1
|
|
||||||
less@2.7.12
|
|
||||||
livedata@1.0.18
|
|
||||||
localstorage@1.2.0
|
localstorage@1.2.0
|
||||||
logging@1.1.20
|
logging@1.2.0
|
||||||
markdown@1.0.12
|
|
||||||
maximum:package-base@1.1.2
|
|
||||||
maximum:server-transform@0.5.0
|
|
||||||
mdg:geolocation@1.3.0
|
mdg:geolocation@1.3.0
|
||||||
meteor@1.9.3
|
meteor@1.9.3
|
||||||
meteor-base@1.4.0
|
meteor-base@1.5.0
|
||||||
meteorhacks:inject-initial@1.0.4
|
minifier-css@1.5.4
|
||||||
meteorhacks:zones@1.6.0
|
minifier-js@2.6.0
|
||||||
meteortesting:browser-tests@0.2.0
|
minimongo@1.7.0
|
||||||
meteortesting:mocha@0.5.1
|
mobile-experience@1.1.0
|
||||||
minifier-css@1.4.3
|
mobile-status-bar@1.1.0
|
||||||
minifier-js@2.5.1
|
|
||||||
minimongo@1.4.5
|
|
||||||
mobile-experience@1.0.5
|
|
||||||
mobile-status-bar@1.0.14
|
|
||||||
modern-browsers@0.1.5
|
modern-browsers@0.1.5
|
||||||
modules@0.14.0
|
modules@0.16.0
|
||||||
modules-runtime@0.11.0
|
modules-runtime@0.12.0
|
||||||
mongo@1.7.0
|
mongo@1.12.0
|
||||||
mongo-decimal@0.1.1
|
mongo-decimal@0.1.2
|
||||||
mongo-dev-server@1.1.0
|
mongo-dev-server@1.1.0
|
||||||
mongo-id@1.0.7
|
mongo-id@1.0.8
|
||||||
mongo-livedata@1.0.12
|
mongo-livedata@1.0.12
|
||||||
mys:fonts@0.0.2
|
mys:fonts@0.0.2
|
||||||
natestrauser:publish-performant-counts@0.1.2
|
natestrauser:publish-performant-counts@0.1.2
|
||||||
nimble:restivus@0.8.12
|
nimble:restivus@0.8.12
|
||||||
npm-bcrypt@0.9.3
|
npm-mongo@3.9.0
|
||||||
npm-mongo@3.2.0
|
|
||||||
nspangler:autoreconnect@0.0.1
|
nspangler:autoreconnect@0.0.1
|
||||||
oauth@1.2.8
|
oauth@2.0.0
|
||||||
oauth2@1.2.1
|
oauth2@1.3.1
|
||||||
observe-sequence@1.0.16
|
observe-sequence@1.0.16
|
||||||
ordered-dict@1.1.0
|
ordered-dict@1.1.0
|
||||||
ostrio:cookies@2.1.3
|
ostrio:cookies@2.7.0
|
||||||
ostrio:mailer@2.1.0
|
ostrio:mailer@2.1.0
|
||||||
ostrio:meteor-root@1.0.7
|
ostrio:meteor-root@1.0.7
|
||||||
peerlibrary:assert@0.2.5
|
|
||||||
peerlibrary:fiber-utils@0.6.0
|
|
||||||
peerlibrary:reactive-mongo@0.1.1
|
|
||||||
peerlibrary:server-autorun@0.5.2
|
|
||||||
percolate:migrations@1.0.2
|
percolate:migrations@1.0.2
|
||||||
practicalmeteor:chai@2.1.0_1
|
promise@0.12.0
|
||||||
practicalmeteor:mocha-core@1.0.1
|
|
||||||
promise@0.11.2
|
|
||||||
raix:eventemitter@0.1.3
|
raix:eventemitter@0.1.3
|
||||||
random@1.1.0
|
random@1.2.0
|
||||||
rate-limit@1.0.9
|
rate-limit@1.0.9
|
||||||
|
react-fast-refresh@0.1.1
|
||||||
react-meteor-data@0.2.16
|
react-meteor-data@0.2.16
|
||||||
reactive-dict@1.3.0
|
reactive-dict@1.3.0
|
||||||
reactive-var@1.0.11
|
reactive-var@1.0.11
|
||||||
reload@1.3.0
|
reload@1.3.1
|
||||||
retry@1.1.0
|
retry@1.1.0
|
||||||
reywood:publish-composite@1.6.0
|
reywood:publish-composite@1.6.0
|
||||||
routepolicy@1.1.0
|
routepolicy@1.1.0
|
||||||
saucecode:timezoned-synced-cron@1.2.11
|
saucecode:timezoned-synced-cron@1.2.11
|
||||||
selaias:cookie-consent@0.4.0
|
selaias:cookie-consent@0.4.0
|
||||||
service-configuration@1.0.11
|
service-configuration@1.1.0
|
||||||
session@1.2.0
|
session@1.2.0
|
||||||
sha@1.0.9
|
sha@1.0.9
|
||||||
shell-server@0.4.0
|
shell-server@0.5.0
|
||||||
simple:json-routes@2.1.0
|
simple:json-routes@1.0.4
|
||||||
socket-stream-client@0.2.2
|
socket-stream-client@0.4.0
|
||||||
spacebars@1.0.15
|
spacebars@1.2.0
|
||||||
spacebars-compiler@1.1.3
|
spacebars-compiler@1.2.0
|
||||||
srp@1.0.12
|
standard-minifier-css@1.7.2
|
||||||
standard-minifier-css@1.5.4
|
standard-minifier-js@2.6.0
|
||||||
standard-minifier-js@2.5.2
|
|
||||||
static-html@1.2.2
|
static-html@1.2.2
|
||||||
templating@1.3.2
|
templating@1.4.1
|
||||||
templating-compiler@1.3.3
|
templating-compiler@1.4.1
|
||||||
templating-runtime@1.3.2
|
templating-runtime@1.5.0
|
||||||
templating-tools@1.1.2
|
templating-tools@1.2.0
|
||||||
themeteorchef:bert@2.1.3
|
themeteorchef:bert@2.1.3
|
||||||
tmeasday:check-npm-versions@0.3.2
|
tmeasday:check-npm-versions@0.3.2
|
||||||
tracker@1.2.0
|
tracker@1.2.0
|
||||||
ui@1.0.13
|
|
||||||
underscore@1.0.10
|
underscore@1.0.10
|
||||||
url@1.2.0
|
url@1.3.2
|
||||||
vjrj:piwik@0.3.1
|
vjrj:piwik@0.3.1
|
||||||
webapp@1.7.5
|
webapp@1.11.0
|
||||||
webapp-hashing@1.0.9
|
webapp-hashing@1.1.0
|
||||||
xolvio:cleaner@0.3.3
|
|
||||||
|
|
|
||||||
29
imports/api/Comments/Comments.js
Normal file
29
imports/api/Comments/Comments.js
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
/* eslint-disable import/no-absolute-path */
|
||||||
|
import { Mongo } from 'meteor/mongo';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Comments collection (fire-page comments).
|
||||||
|
*
|
||||||
|
* Replaces the abandoned `arkham:comments-ui` Atmosphere package (no Meteor 2.x
|
||||||
|
* build) with a small React + methods implementation. The collection name
|
||||||
|
* ('comments') and the core document shape are kept so existing production
|
||||||
|
* documents keep rendering:
|
||||||
|
*
|
||||||
|
* { referenceId: 'fire-<id>', content, userId, username,
|
||||||
|
* media: { type, content }, likes: [userId], dislikes: [userId],
|
||||||
|
* status: 'approved', createdAt, updatedAt }
|
||||||
|
*
|
||||||
|
* Legacy documents may also carry replies[]/starRatings[]/ratingScore/
|
||||||
|
* isAnonymous — those features were disabled in this app's config and are
|
||||||
|
* ignored by the new UI.
|
||||||
|
*/
|
||||||
|
const Comments = new Mongo.Collection('comments');
|
||||||
|
|
||||||
|
// Writes only through the vetted server methods.
|
||||||
|
Comments.deny({
|
||||||
|
insert: () => true,
|
||||||
|
update: () => true,
|
||||||
|
remove: () => true
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Comments;
|
||||||
44
imports/api/Comments/mediaAnalyzers.js
Normal file
44
imports/api/Comments/mediaAnalyzers.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Media analyzers ported from the old arkham:comments-ui package
|
||||||
|
* (lib/services/media-analyzers). Given a comment's text, detect an embeddable
|
||||||
|
* image or YouTube URL and return { type, content }. First match wins.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const imageAnalyzer = {
|
||||||
|
name: 'image',
|
||||||
|
getMediaFromContent(content) {
|
||||||
|
if (content) {
|
||||||
|
const urls = content.match(/(\S+\.[^/\s]+(\/\S+|\/|))(.jpg|.png|.gif)/g);
|
||||||
|
if (urls && urls[0]) return urls[0];
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const youtubeAnalyzer = {
|
||||||
|
name: 'youtube',
|
||||||
|
getMediaFromContent(content) {
|
||||||
|
const parts = (content || '').match(/(?:https?:\/\/)?(?:www\.youtube\.com|youtu\.?be)\/([\w=?]+)/);
|
||||||
|
let mediaContent = '';
|
||||||
|
if (parts && parts[1]) {
|
||||||
|
let id = parts[1];
|
||||||
|
if (id.indexOf('v=') > -1) {
|
||||||
|
const subParts = id.match(/v=([\w]+)/);
|
||||||
|
if (subParts && subParts[1]) id = subParts[1];
|
||||||
|
}
|
||||||
|
mediaContent = `https://www.youtube.com/embed/${id}`;
|
||||||
|
}
|
||||||
|
return mediaContent;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const analyzers = [imageAnalyzer, youtubeAnalyzer];
|
||||||
|
|
||||||
|
// Returns { type, content } or {}.
|
||||||
|
export default function getMediaFromContent(content) {
|
||||||
|
for (let i = 0; i < analyzers.length; i += 1) {
|
||||||
|
const mediaContent = analyzers[i].getMediaFromContent(content);
|
||||||
|
if (mediaContent) return { type: analyzers[i].name, content: mediaContent };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
3
imports/api/Comments/server/index.js
Normal file
3
imports/api/Comments/server/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
// Server-side registration for the Comments feature.
|
||||||
|
import './methods';
|
||||||
|
import './publications';
|
||||||
102
imports/api/Comments/server/methods.js
Normal file
102
imports/api/Comments/server/methods.js
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
/* eslint-disable import/no-absolute-path */
|
||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
import { check } from 'meteor/check';
|
||||||
|
import Comments from '/imports/api/Comments/Comments';
|
||||||
|
import getMediaFromContent from '/imports/api/Comments/mediaAnalyzers';
|
||||||
|
import rateLimit from '/imports/modules/rate-limit';
|
||||||
|
import onCommentAdd from '/imports/api/Comments/server/onCommentAdd';
|
||||||
|
|
||||||
|
const MAX_LEN = 5000;
|
||||||
|
|
||||||
|
function usernameOf(user) {
|
||||||
|
return (user && user.profile && user.profile.name && user.profile.name.first)
|
||||||
|
? user.profile.name.first
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireOwner(commentId, userId) {
|
||||||
|
const comment = Comments.findOne(commentId);
|
||||||
|
if (!comment) throw new Meteor.Error('404', 'Comment not found');
|
||||||
|
if (comment.userId !== userId) throw new Meteor.Error('403', 'Not your comment');
|
||||||
|
return comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(commentId, field, otherField, userId) {
|
||||||
|
check(commentId, String);
|
||||||
|
if (!userId) throw new Meteor.Error('403', 'Login required');
|
||||||
|
const comment = Comments.findOne(commentId);
|
||||||
|
if (!comment) throw new Meteor.Error('404', 'Comment not found');
|
||||||
|
const has = (comment[field] || []).includes(userId);
|
||||||
|
const modifier = has
|
||||||
|
? { $pull: { [field]: userId } }
|
||||||
|
: { $addToSet: { [field]: userId }, $pull: { [otherField]: userId } };
|
||||||
|
Comments.update(commentId, modifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
Meteor.methods({
|
||||||
|
'comments.insert': function commentsInsert(referenceId, content) {
|
||||||
|
check(referenceId, String);
|
||||||
|
check(content, String);
|
||||||
|
if (!this.userId) throw new Meteor.Error('403', 'Login required to comment');
|
||||||
|
const text = content.trim();
|
||||||
|
if (!text) throw new Meteor.Error('400', 'Empty comment');
|
||||||
|
if (text.length > MAX_LEN) throw new Meteor.Error('400', 'Comment too long');
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const doc = {
|
||||||
|
referenceId,
|
||||||
|
content: text,
|
||||||
|
userId: this.userId,
|
||||||
|
username: usernameOf(Meteor.users.findOne(this.userId)),
|
||||||
|
media: getMediaFromContent(text),
|
||||||
|
likes: [],
|
||||||
|
dislikes: [],
|
||||||
|
status: 'approved',
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now
|
||||||
|
};
|
||||||
|
const _id = Comments.insert(doc);
|
||||||
|
// Fire-and-forget: notify other commenters of this fire. Never let a mail
|
||||||
|
// failure break the insert.
|
||||||
|
try {
|
||||||
|
onCommentAdd({ ...doc, _id });
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`comments onCommentAdd failed: ${e}`);
|
||||||
|
}
|
||||||
|
return _id;
|
||||||
|
},
|
||||||
|
|
||||||
|
'comments.edit': function commentsEdit(commentId, content) {
|
||||||
|
check(commentId, String);
|
||||||
|
check(content, String);
|
||||||
|
if (!this.userId) throw new Meteor.Error('403', 'Login required');
|
||||||
|
requireOwner(commentId, this.userId);
|
||||||
|
const text = content.trim();
|
||||||
|
if (!text) throw new Meteor.Error('400', 'Empty comment');
|
||||||
|
if (text.length > MAX_LEN) throw new Meteor.Error('400', 'Comment too long');
|
||||||
|
Comments.update(commentId, {
|
||||||
|
$set: { content: text, media: getMediaFromContent(text), updatedAt: new Date() }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
'comments.remove': function commentsRemove(commentId) {
|
||||||
|
check(commentId, String);
|
||||||
|
if (!this.userId) throw new Meteor.Error('403', 'Login required');
|
||||||
|
requireOwner(commentId, this.userId);
|
||||||
|
Comments.remove(commentId);
|
||||||
|
},
|
||||||
|
|
||||||
|
'comments.like': function commentsLike(commentId) {
|
||||||
|
toggle(commentId, 'likes', 'dislikes', this.userId);
|
||||||
|
},
|
||||||
|
|
||||||
|
'comments.dislike': function commentsDislike(commentId) {
|
||||||
|
toggle(commentId, 'dislikes', 'likes', this.userId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
rateLimit({
|
||||||
|
methods: ['comments.insert', 'comments.edit', 'comments.remove', 'comments.like', 'comments.dislike'],
|
||||||
|
limit: 5,
|
||||||
|
timeRange: 1000
|
||||||
|
});
|
||||||
42
imports/api/Comments/server/onCommentAdd.js
Normal file
42
imports/api/Comments/server/onCommentAdd.js
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
/* eslint-disable import/no-absolute-path */
|
||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
import i18n from 'i18next';
|
||||||
|
import sendEmail, { subjectTruncate } from '/imports/modules/server/send-email';
|
||||||
|
import getEmailOf from '/imports/modules/get-email-of-user';
|
||||||
|
import Comments from '/imports/api/Comments/Comments';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When a comment is added to a fire, email the other users who commented on the
|
||||||
|
* same fire (uniq, excluding the author). Ported from the old
|
||||||
|
* startup/server/comments.js `onEvent` handler.
|
||||||
|
*/
|
||||||
|
export default function onCommentAdd(payload) {
|
||||||
|
const { referenceId, userId } = payload;
|
||||||
|
const query = { referenceId, userId: { $ne: userId } };
|
||||||
|
|
||||||
|
Comments.rawCollection().distinct('userId', query).then((users) => {
|
||||||
|
const path = referenceId.replace(/fire-/, 'fire/archive/');
|
||||||
|
const fireUrl = Meteor.absoluteUrl(path);
|
||||||
|
Meteor.users.find({ _id: { $in: users } }).forEach((user) => {
|
||||||
|
const { firstName, emailAddress } = getEmailOf(user);
|
||||||
|
if (emailAddress) {
|
||||||
|
const emailOpts = {
|
||||||
|
to: emailAddress,
|
||||||
|
subject: subjectTruncate.apply(i18n.t('Hay más información sobre un fuego')),
|
||||||
|
lang: user.lang,
|
||||||
|
template: 'new-fire-comment',
|
||||||
|
templateVars: {
|
||||||
|
applicationName: i18n.t('AppName'),
|
||||||
|
firstName,
|
||||||
|
fireUrl
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sendEmail(emailOpts).catch((error) => {
|
||||||
|
console.warn(`comments new-fire-comment mail failed: ${error}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
console.warn(`comments distinct() failed: ${error}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
14
imports/api/Comments/server/publications.js
Normal file
14
imports/api/Comments/server/publications.js
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
/* eslint-disable import/no-absolute-path */
|
||||||
|
/* eslint-disable prefer-arrow-callback */
|
||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
import { check } from 'meteor/check';
|
||||||
|
import Comments from '/imports/api/Comments/Comments';
|
||||||
|
|
||||||
|
// Comments for one reference (a fire page), oldest first.
|
||||||
|
Meteor.publish('comments.forReference', function commentsForReference(referenceId) {
|
||||||
|
check(referenceId, String);
|
||||||
|
return Comments.find(
|
||||||
|
{ referenceId },
|
||||||
|
{ sort: { createdAt: 1 } }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -84,7 +84,7 @@ const find = (collection, northEastLng, northEastLat, southWestLng, southWestLat
|
||||||
return fires;
|
return fires;
|
||||||
};
|
};
|
||||||
|
|
||||||
Meteor.publishTransformed('falsePositivesMyloc', function falsePositivesInMyLoc(northEastLng, northEastLat, southWestLng, southWestLat) {
|
Meteor.publish('falsePositivesMyloc', function falsePositivesInMyLoc(northEastLng, northEastLat, southWestLng, southWestLat) {
|
||||||
// latitude -90 and 90 and the longitude between -180 and 180
|
// latitude -90 and 90 and the longitude between -180 and 180
|
||||||
check(northEastLng, NumberBetween(-180, 180));
|
check(northEastLng, NumberBetween(-180, 180));
|
||||||
check(southWestLat, NumberBetween(-90, 90));
|
check(southWestLat, NumberBetween(-90, 90));
|
||||||
|
|
@ -94,7 +94,7 @@ Meteor.publishTransformed('falsePositivesMyloc', function falsePositivesInMyLoc(
|
||||||
return find(FalsePositives, northEastLng, northEastLat, southWestLng, southWestLat);
|
return find(FalsePositives, northEastLng, northEastLat, southWestLng, southWestLat);
|
||||||
});
|
});
|
||||||
|
|
||||||
Meteor.publishTransformed('industriesMyloc', function industriesInMyLoc(northEastLng, northEastLat, southWestLng, southWestLat) {
|
Meteor.publish('industriesMyloc', function industriesInMyLoc(northEastLng, northEastLat, southWestLng, southWestLat) {
|
||||||
// latitude -90 and 90 and the longitude between -180 and 180
|
// latitude -90 and 90 and the longitude between -180 and 180
|
||||||
check(northEastLng, NumberBetween(-180, 180));
|
check(northEastLng, NumberBetween(-180, 180));
|
||||||
check(southWestLat, NumberBetween(-90, 90));
|
check(southWestLat, NumberBetween(-90, 90));
|
||||||
|
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
/* global Comments */
|
|
||||||
/* eslint-disable import/no-absolute-path */
|
|
||||||
|
|
||||||
import i18n from '/imports/startup/client/i18n';
|
|
||||||
import '/imports/startup/common/comments';
|
|
||||||
|
|
||||||
import './comments.scss';
|
|
||||||
|
|
||||||
i18n.init((err, t) => {
|
|
||||||
Comments.ui.setContent({
|
|
||||||
title: ' ', // i18n.t('Comentarios'),
|
|
||||||
save: t('Guardar'),
|
|
||||||
reply: t('Responder'),
|
|
||||||
edit: t('Editar'),
|
|
||||||
remove: t('Borrar'),
|
|
||||||
'placeholder-textarea': t('Añadir un comentario'),
|
|
||||||
'add-button-reply': t('Añadir una respuesta'),
|
|
||||||
'add-button': t('Añadir comentario'),
|
|
||||||
'you-need-to-login': t('Necesitas iniciar sesión para'),
|
|
||||||
'add comments': t('añadir comentarios'),
|
|
||||||
'like comments': t('puntuar comentarios'),
|
|
||||||
'rate comments': t('puntuar comentarios'),
|
|
||||||
'add replies': t('responder'),
|
|
||||||
'load-more': t('Más comentarios')
|
|
||||||
});
|
|
||||||
|
|
||||||
/* This in client side */
|
|
||||||
Comments.ui.config({
|
|
||||||
limit: 20, // default 10
|
|
||||||
loadMoreCount: 20, // default 20
|
|
||||||
/* generateAvatar: function genAvatar(user, isAnonymous) {
|
|
||||||
if (isAnonymous) {
|
|
||||||
return i18n.t('Anónimo');
|
|
||||||
}
|
|
||||||
return user.profile && user.profile.name && user.profile.name.first ? user.profile.name.first : null;
|
|
||||||
}, */
|
|
||||||
template: 'bootstrap', // default 'semantic-ui'
|
|
||||||
// default 'http://s3.amazonaws.com/37assets/svn/765-default-avatar.png'
|
|
||||||
defaultAvatar: '/default-avatar.png',
|
|
||||||
markdown: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
.comments-section {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comments-box {
|
|
||||||
max-width: inherit;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.comment-content {
|
|
||||||
/* white-space: pre-line; */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* remove comment btn danger style */
|
|
||||||
|
|
||||||
div.media-body.comment> div> div> div.btn.btn-danger.remove-action {
|
|
||||||
background-color: white;
|
|
||||||
border: 1px solid rgb(204, 204, 204);
|
|
||||||
color: rgb(51, 51, 51);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* text-area height */
|
|
||||||
.form-control .create-comment {
|
|
||||||
height: 9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.img-avatar {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
/* global Comments */
|
|
||||||
|
|
||||||
// Client and Server
|
|
||||||
Comments.config({
|
|
||||||
rating: 'likes-and-dislikes',
|
|
||||||
allowAnonymous: () => false,
|
|
||||||
allowReplies: () => false, // disabled right now: I don't know hot to get referenceId of replies
|
|
||||||
anonymousSalt: 'klasddl3lala0l3lasdlas0ol3lasdlao3lasdoaslaldal3lasdclasdlal3lasdladlaq',
|
|
||||||
publishUserFields: {
|
|
||||||
profile: 1
|
|
||||||
},
|
|
||||||
mediaAnalyzers: [
|
|
||||||
Comments.analyzers.image,
|
|
||||||
Comments.analyzers.youtube
|
|
||||||
],
|
|
||||||
generateUsername: function genUser(user) {
|
|
||||||
// console.log(JSON.stringify(user));
|
|
||||||
// FIXME
|
|
||||||
return user.profile && user.profile.name && user.profile.name.first ? user.profile.name.first : '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
/* global Comments */
|
|
||||||
/* eslint-disable import/no-absolute-path */
|
|
||||||
|
|
||||||
import { Meteor } from 'meteor/meteor';
|
|
||||||
import i18n from 'i18next';
|
|
||||||
import sendEmail, { subjectTruncate } from '/imports/modules/server/send-email';
|
|
||||||
import getEmailOf from '/imports/modules/get-email-of-user';
|
|
||||||
import '../common/comments';
|
|
||||||
|
|
||||||
/* import i18n from 'i18next';
|
|
||||||
import moment from 'moment';
|
|
||||||
import { dateLongFormat } from '/imports/api/Common/dates';
|
|
||||||
import Notifications from '/imports/api/Notifications/Notifications';
|
|
||||||
// import sendMail from '/imports/startup/server/email';
|
|
||||||
// import { hr } from '/imports/startup/server/email';
|
|
||||||
import getOAuthProfile from '/imports/modules/get-oauth-profile';
|
|
||||||
import image from 'google-maps-image-api-url';
|
|
||||||
import { trim } from '/imports/ui/components/NotificationsObserver/util.js'; */
|
|
||||||
|
|
||||||
Meteor.startup(() => {
|
|
||||||
// On Server or Client (preferably on Server)
|
|
||||||
Comments.config({
|
|
||||||
onEvent: (name, action, payload) => {
|
|
||||||
// e.g send a mail
|
|
||||||
// console.log(`name: ${name}, action: ${action}, payload: ${JSON.stringify(payload)}`);
|
|
||||||
|
|
||||||
// Samples:
|
|
||||||
|
|
||||||
// name: comment, action: add, payload: {"referenceId":"fire-56a9cc5a5586879eb9cc6dd7","content":"asdfasdf asd fasd fas","userId":"tdK4e43ikjDXct7Gj","isAnonymous":false,"createdAt":"2018-03-04T14:53:40.319Z","likes":[],"dislikes":[],"replies":[],"media":{},"status":"approved","starRatings":[],"ratingScore":0,"_id":"mfAe3HL9qNofhiAnh"}
|
|
||||||
// name: reply, action: add, payload: {"replyId":"teSLaz4f8RxHjptBm","content":"sas fasdfasdasdf","userId":"tdK4e43ikjDXct7Gj","createdAt":"2018-03-04T14:54:56.965Z","replies":[],"likes":[],"lastUpdatedAt":"2018-03-04T14:54:56.965Z","isAnonymous":false,"status":"approved","media":{},"ratingScore":0,"_id":"DyHt28rF6rTDnC5fP","rootUserId":"tdK4e43ikjDXct7Gj"}
|
|
||||||
// name: reply, action: like, payload: {"replyId":"teSLaz4f8RxHjptBm","content":"sas fasdfasdasdf","userId":"tdK4e43ikjDXct7Gj","createdAt":"2018-03-04T14:54:56.965Z","replies":[],"likes":["tdK4e43ikjDXct7Gj"],"lastUpdatedAt":"2018-03-04T14:54:56.965Z","isAnonymous":false,"status":"approved","media":{},"ratingScore":1,"_id":"DyHt28rF6rTDnC5fP","ratedUserId":"tdK4e43ikjDXct7Gj","rootUserId":"tdK4e43ikjDXct7Gj"}
|
|
||||||
// name: comment, action: like, payload: {"_id":"mfAe3HL9qNofhiAnh","ratedUserId":"tdK4e43ikjDXct7Gj"}
|
|
||||||
// name: reply, action: edit, payload: {"_id":"DyHt28rF6rTDnC5fP","replyId":"teSLaz4f8RxHjptBm","content":"sas fasdfasdasdf dasd fadsfa","userId":"tdK4e43ikjDXct7Gj","createdAt":"2018-03-04T14:54:56.965Z","replies":[],"likes":[],"lastUpdatedAt":"2018-03-04T14:54:56.965Z","isAnonymous":false,"status":"approved","media":{},"ratingScore":0,"starRatings":[],"ratedUserId":"tdK4e43ikjDXct7Gj","rootUserId":"tdK4e43ikjDXct7Gj"}
|
|
||||||
// name: reply, action: remove, payload: {"_id":"DyHt28rF6rTDnC5fP","rootUserId":"tdK4e43ikjDXct7Gj"}
|
|
||||||
|
|
||||||
// name: (comment|reply)
|
|
||||||
// action: (add|like|edit|remove)
|
|
||||||
|
|
||||||
if (name === 'comment' && action.match(/add|like|edit/)) {
|
|
||||||
if (action === 'add') {
|
|
||||||
// Check for other users that did comments and send an email (uniq users, and not this user)
|
|
||||||
// console.log(payload.referenceId);
|
|
||||||
const query = payload.isAnonymous ?
|
|
||||||
{ referenceId: payload.referenceId } :
|
|
||||||
{ referenceId: payload.referenceId, userId: { $ne: payload.userId } };
|
|
||||||
Comments.getCollection().rawCollection().distinct('userId', query).then((users) => {
|
|
||||||
// console.log(users);
|
|
||||||
const path = payload.referenceId.replace(/fire-/, 'fire/archive/');
|
|
||||||
const fireUrl = Meteor.absoluteUrl(path);
|
|
||||||
// console.log(fireUrl);
|
|
||||||
Meteor.users.find({ _id: { $in: users } }).forEach((user) => {
|
|
||||||
const { firstName, emailAddress } = getEmailOf(user);
|
|
||||||
// console.log(JSON.stringify(user));
|
|
||||||
if (emailAddress) {
|
|
||||||
const emailOpts = {
|
|
||||||
to: emailAddress,
|
|
||||||
subject: subjectTruncate.apply(i18n.t('Hay más información sobre un fuego')),
|
|
||||||
lang: user.lang,
|
|
||||||
template: 'new-fire-comment',
|
|
||||||
templateVars: {
|
|
||||||
applicationName: i18n.t('AppName'),
|
|
||||||
firstName,
|
|
||||||
fireUrl
|
|
||||||
}
|
|
||||||
};
|
|
||||||
sendEmail(emailOpts).catch((error) => {
|
|
||||||
throw new Meteor.Error('500', `${error}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (name === 'reply' && action.match(/add|like|edit/)) {
|
|
||||||
// Check for other previous users to this thread and send an email
|
|
||||||
// console.log('Reply op');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -6,10 +6,10 @@ import './accounts';
|
||||||
import './api';
|
import './api';
|
||||||
import './fixtures';
|
import './fixtures';
|
||||||
import './email';
|
import './email';
|
||||||
|
import '/imports/api/Comments/server';
|
||||||
import './IPGeocoder';
|
import './IPGeocoder';
|
||||||
import './migrations';
|
import './migrations';
|
||||||
import './facts';
|
import './facts';
|
||||||
import './comments';
|
|
||||||
import './sitemaps';
|
import './sitemaps';
|
||||||
import './subsUnion';
|
import './subsUnion';
|
||||||
import './prerender';
|
import './prerender';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
/* global Migrations, Comments */
|
/* global Migrations */
|
||||||
/* eslint-disable import/no-absolute-path */
|
/* eslint-disable import/no-absolute-path */
|
||||||
import { Meteor } from 'meteor/meteor';
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
import Comments from '/imports/api/Comments/Comments';
|
||||||
import { Accounts } from 'meteor/accounts-base';
|
import { Accounts } from 'meteor/accounts-base';
|
||||||
import randomHex from 'crypto-random-hex';
|
import randomHex from 'crypto-random-hex';
|
||||||
import UserSubsToFiresCollection from '/imports/api/Subscriptions/Subscriptions';
|
import UserSubsToFiresCollection from '/imports/api/Subscriptions/Subscriptions';
|
||||||
|
|
@ -163,7 +164,7 @@ Meteor.startup(() => {
|
||||||
Migrations.add({
|
Migrations.add({
|
||||||
version: 11,
|
version: 11,
|
||||||
up: function noAnonComments() {
|
up: function noAnonComments() {
|
||||||
Comments.getCollection().remove({ isAnonymous: true });
|
Comments.remove({ isAnonymous: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
/* global sitemaps Comments */
|
/* global sitemaps */
|
||||||
/* eslint-disable import/no-absolute-path */
|
/* eslint-disable import/no-absolute-path */
|
||||||
|
|
||||||
import Fires from '/imports/api/Fires/Fires';
|
import Fires from '/imports/api/Fires/Fires';
|
||||||
// import Comments from '/imports/api/Comments/Comments';
|
import Comments from '/imports/api/Comments/Comments';
|
||||||
|
|
||||||
const firesMapEnabled = false;
|
const firesMapEnabled = false;
|
||||||
|
|
||||||
|
|
@ -32,7 +32,7 @@ sitemaps.add('/sitemap.xml', () => {
|
||||||
if (firesMapEnabled) {
|
if (firesMapEnabled) {
|
||||||
Fires.find({}, { limit: 100, sort: { createdAt: -1 } }).fetch().forEach((fire) => {
|
Fires.find({}, { limit: 100, sort: { createdAt: -1 } }).fetch().forEach((fire) => {
|
||||||
// Search the last comment of tha fire
|
// Search the last comment of tha fire
|
||||||
const lastComment = Comments.getCollection().findOne({ referenceId: `fire-${fire._id}` }, { sort: { createdAt: -1 } });
|
const lastComment = Comments.findOne({ referenceId: `fire-${fire._id}` }, { sort: { createdAt: -1 } });
|
||||||
out.push({
|
out.push({
|
||||||
page: `/fire/archive/${fire._id._str}`,
|
page: `/fire/archive/${fire._id._str}`,
|
||||||
lastmod: lastComment ? lastComment.createdAt : fire.updatedAt
|
lastmod: lastComment ? lastComment.createdAt : fire.updatedAt
|
||||||
|
|
|
||||||
76
imports/ui/components/Comments/Comments.scss
Normal file
76
imports/ui/components/Comments/Comments.scss
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
.comments-section {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comments-box {
|
||||||
|
max-width: inherit;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comments-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment {
|
||||||
|
padding: 0.75em 0;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-author {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-date {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.comment-content {
|
||||||
|
white-space: pre-line;
|
||||||
|
margin: 0.4em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-media-image {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-media-youtube iframe {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 240px;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-actions .btn {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-actions .btn.active {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* keep the previous "remove" button neutral (not danger red) */
|
||||||
|
.comment-owner-actions .remove-action {
|
||||||
|
color: rgb(51, 51, 51);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-form .create-comment {
|
||||||
|
height: 9em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-login-hint {
|
||||||
|
color: #888;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-avatar {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
152
imports/ui/components/Comments/CommentsBox.js
Normal file
152
imports/ui/components/Comments/CommentsBox.js
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
/* 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);
|
||||||
|
|
@ -12,14 +12,13 @@ import { Bert } from 'meteor/themeteorchef:bert';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { Map, GeoJSON } from 'react-leaflet';
|
import { Map, GeoJSON } from 'react-leaflet';
|
||||||
import { rectangleAround } from 'map-common-utils';
|
import { rectangleAround } from 'map-common-utils';
|
||||||
import Blaze from 'meteor/gadicc:blaze-react-component';
|
|
||||||
import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers';
|
import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers';
|
||||||
import NotFound from '/imports/ui/pages/NotFound/NotFound';
|
import NotFound from '/imports/ui/pages/NotFound/NotFound';
|
||||||
import FiresCollection from '/imports/api/Fires/Fires';
|
import FiresCollection from '/imports/api/Fires/Fires';
|
||||||
import FireList from '/imports/ui/components/Maps/FireList';
|
import FireList from '/imports/ui/components/Maps/FireList';
|
||||||
import FromNow from '/imports/ui/components/FromNow/FromNow';
|
import FromNow from '/imports/ui/components/FromNow/FromNow';
|
||||||
import { dateLongFormat, dateYYYYMMDD } from '/imports/api/Common/dates';
|
import { dateLongFormat, dateYYYYMMDD } from '/imports/api/Common/dates';
|
||||||
import '/imports/startup/client/comments';
|
import CommentsBox from '/imports/ui/components/Comments/CommentsBox';
|
||||||
import FalsePositiveTypes from '/imports/api/FalsePositives/FalsePositiveTypes';
|
import FalsePositiveTypes from '/imports/api/FalsePositives/FalsePositiveTypes';
|
||||||
import FalsePositivesCollection, { falsePositivesRemap } from '/imports/api/FalsePositives/FalsePositives';
|
import FalsePositivesCollection, { falsePositivesRemap } from '/imports/api/FalsePositives/FalsePositives';
|
||||||
import IndustriesCollection, { industriesRemap } from '/imports/api/Industries/Industries';
|
import IndustriesCollection, { industriesRemap } from '/imports/api/Industries/Industries';
|
||||||
|
|
@ -233,7 +232,7 @@ class Fire extends React.Component {
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="comments-section">
|
<div className="comments-section">
|
||||||
<Blaze template="commentsBox" id={`fire-${fire._id}`} />
|
<CommentsBox referenceId={`fire-${fire._id}`} />
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 4761da921f467838a2c01b34e84a7703ee49b12c
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// We need a function that treats thrown errors exactly like Iron Router would.
|
||||||
|
// This file is written in JavaScript to enable copy-pasting Iron Router code.
|
||||||
|
|
||||||
|
// Taken from: https://github.com/iron-meteor/iron-router/blob/9c369499c98af9fd12ef9e68338dee3b1b1276aa/lib/router_server.js#L3
|
||||||
|
var env = process.env.NODE_ENV || 'development';
|
||||||
|
|
||||||
|
// Taken from: https://github.com/iron-meteor/iron-router/blob/9c369499c98af9fd12ef9e68338dee3b1b1276aa/lib/router_server.js#L47
|
||||||
|
ironRouterSendErrorToResponse = function (err, req, res) {
|
||||||
|
if (res.statusCode < 400)
|
||||||
|
res.statusCode = 500;
|
||||||
|
|
||||||
|
if (err.status)
|
||||||
|
res.statusCode = err.status;
|
||||||
|
|
||||||
|
if (env === 'development')
|
||||||
|
msg = (err.stack || err.toString()) + '\n';
|
||||||
|
else
|
||||||
|
//XXX get this from standard dict of error messages?
|
||||||
|
msg = 'Server error.';
|
||||||
|
|
||||||
|
console.error(err.stack || err.toString());
|
||||||
|
|
||||||
|
if (res.headersSent)
|
||||||
|
return req.socket.destroy();
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/html');
|
||||||
|
res.setHeader('Content-Length', Buffer.byteLength(msg));
|
||||||
|
if (req.method === 'HEAD')
|
||||||
|
return res.end();
|
||||||
|
res.end(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
1010
packages/nimble-restivus/lib/restivus-all.js
Normal file
1010
packages/nimble-restivus/lib/restivus-all.js
Normal file
File diff suppressed because it is too large
Load diff
34
packages/nimble-restivus/package.js
Normal file
34
packages/nimble-restivus/package.js
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
Package.describe({
|
||||||
|
name: 'nimble:restivus',
|
||||||
|
summary: 'Create authenticated REST APIs in Meteor via HTTP/HTTPS. (Vendored fork: precompiled JS, accounts-password 2.x compatible.)',
|
||||||
|
version: '0.8.12',
|
||||||
|
git: 'https://github.com/kahmali/meteor-restivus.git'
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Vendored fork of nimble:restivus@0.8.12.
|
||||||
|
*
|
||||||
|
* Why: the published Atmosphere package pins accounts-password@1.3.3, which
|
||||||
|
* conflicts with Meteor 2.x (accounts-password 2.0). The upstream source is
|
||||||
|
* CoffeeScript, and the coffeescript build plugin crashes this build machine
|
||||||
|
* (node_contextify assertion). So the .coffee sources were precompiled to plain
|
||||||
|
* JS (lib/restivus-all.js) and this package.js drops the coffeescript build
|
||||||
|
* dependency and loosens the accounts-password constraint to 2.x.
|
||||||
|
*
|
||||||
|
* The REST behavior is unchanged — verified by the REST smoke test.
|
||||||
|
*/
|
||||||
|
Package.onUse(function (api) {
|
||||||
|
api.versionsFrom('METEOR@2.3');
|
||||||
|
|
||||||
|
api.use('check');
|
||||||
|
api.use('underscore');
|
||||||
|
api.use('accounts-password@2.0.0');
|
||||||
|
api.use('simple:json-routes');
|
||||||
|
|
||||||
|
// iron-router error helper defines the `ironRouterSendErrorToResponse` global
|
||||||
|
// used by the routes, so it must load first.
|
||||||
|
api.addFiles('lib/iron-router-error-to-response.js', 'server');
|
||||||
|
api.addFiles('lib/restivus-all.js', 'server');
|
||||||
|
|
||||||
|
api.export('Restivus', 'server');
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue