diff --git a/.meteor/packages b/.meteor/packages index 43f3dc2..399b4a7 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -4,33 +4,33 @@ # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. -meteor-base@1.4.0 # Packages every Meteor app needs to have -mobile-experience@1.0.5 # Packages for a great mobile UX -mongo@1.7.0 # The database Meteor supports right now +meteor-base@1.5.0 # Packages every Meteor app needs to have +mobile-experience@1.1.0 # Packages for a great mobile UX +mongo@1.12.0 # The database Meteor supports right now reactive-var@1.0.11 # Reactive variable for tracker 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-js@2.5.2 # JS minifier run for production mode +standard-minifier-css@1.7.2 # CSS 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. -ecmascript@0.13.2 # Enable ECMAScript2015+ syntax in app code -shell-server@0.4.0 # Server-side component of the `meteor shell` command +ecmascript@0.15.2 # Enable ECMAScript2015+ syntax in app code +shell-server@0.5.0 # Server-side component of the `meteor shell` command react-meteor-data alanning:roles -fourseven:scss -accounts-base@1.4.5 -accounts-password@1.5.2 -service-configuration@1.0.11 -accounts-facebook@1.3.2 -accounts-github@1.4.2 -accounts-google@1.3.3 +fourseven:scss@4.14.1 +accounts-base@2.0.0 +accounts-password@2.0.0 +service-configuration@1.1.0 +accounts-facebook@1.3.3 +accounts-github@1.5.0 +accounts-google@1.4.0 themeteorchef:bert fortawesome:fontawesome aldeed:collection2-core@2.0.1 audit-argument-checks@1.0.7 -ddp-rate-limiter@1.0.7 -dynamic-import@0.5.1 +ddp-rate-limiter@1.1.0 +dynamic-import@0.7.0 static-html alexwine:bootstrap-4 @@ -42,29 +42,19 @@ flowkey:raven # js errors vjrj:piwik # Stats mdg:geolocation natestrauser:publish-performant-counts -maximum:server-transform mys:fonts # fonst in npm ostrio:mailer # mailer # babrahams:constellation # dev utility percolate:migrations # db migrations ostrio:meteor-root -meteortesting:mocha -practicalmeteor:chai aldeed:schema-deny -aldeed:template-extension dburles:collection-helpers -less@2.7.11 -markdown@1.0.12 mongo-livedata@1.0.12 reywood:publish-composite -barbatus:stars-rating -arkham:comments-ui facts@1.0.9 gadicohen:sitemaps nspangler:autoreconnect saucecode:timezoned-synced-cron # lmachens:kadira -meteorhacks:zones -nimble:restivus -xolvio:cleaner -underscore +nimble:restivus@0.8.12 +underscore@1.0.10 diff --git a/.meteor/release b/.meteor/release index bfccdc2..2a30e8f 100644 --- a/.meteor/release +++ b/.meteor/release @@ -1 +1 @@ -METEOR@1.8.3 +METEOR@2.3 diff --git a/.meteor/versions b/.meteor/versions index ca44053..d11cbbb 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -1,157 +1,128 @@ 255kb:meteor-status@1.5.0 -accounts-base@1.4.5 -accounts-facebook@1.3.2 -accounts-github@1.4.2 -accounts-google@1.3.3 -accounts-oauth@1.1.16 -accounts-password@1.5.2 -alanning:roles@1.2.16 +accounts-base@2.0.1 +accounts-facebook@1.3.3 +accounts-github@1.5.0 +accounts-google@1.4.0 +accounts-oauth@1.3.0 +accounts-password@2.0.1 +alanning:roles@1.3.0 aldeed:collection2-core@2.1.2 aldeed:schema-deny@2.0.1 -aldeed:template-extension@4.1.0 alexwine:bootstrap-4@4.1.0 allow-deny@1.1.0 -arkham:comments-ui@1.4.4 audit-argument-checks@1.0.7 -autoupdate@1.6.0 -babel-compiler@7.4.2 -babel-runtime@1.4.0 -barbatus:stars-rating@1.1.1 +autoupdate@1.7.0 +babel-compiler@7.6.2 +babel-runtime@1.5.0 base64@1.0.12 binary-heap@1.0.11 -blaze@2.3.3 -blaze-tools@1.0.10 -boilerplate-generator@1.6.0 -browser-policy@1.1.0 -browser-policy-common@1.0.11 -browser-policy-content@1.1.0 -browser-policy-framing@1.1.0 -caching-compiler@1.2.1 -caching-html-compiler@1.1.2 -callback-hook@1.2.0 +blaze@2.5.0 +blaze-tools@1.1.0 +boilerplate-generator@1.7.1 +caching-compiler@1.2.2 +caching-html-compiler@1.2.0 +callback-hook@1.3.0 check@1.3.1 -coffeescript@1.0.17 dburles:collection-helpers@1.1.0 -dburles:mongo-collection-instances@0.3.5 ddp@1.4.0 -ddp-client@2.3.3 +ddp-client@2.5.0 ddp-common@1.4.0 -ddp-rate-limiter@1.0.7 -ddp-server@2.3.0 -deps@1.0.12 +ddp-rate-limiter@1.1.0 +ddp-server@2.4.0 diff-sequence@1.1.1 -dynamic-import@0.5.1 -ecmascript@0.13.2 +dynamic-import@0.7.0 +ecmascript@0.15.2 ecmascript-runtime@0.7.0 -ecmascript-runtime-client@0.9.0 -ecmascript-runtime-server@0.8.0 -ejson@1.1.0 -email@1.2.3 +ecmascript-runtime-client@0.11.1 +ecmascript-runtime-server@0.10.1 +ejson@1.1.1 +email@2.1.0 es5-shim@4.8.0 -facebook-oauth@1.6.0 +facebook-oauth@1.9.0 facts@1.0.9 fetch@0.1.1 flowkey:raven@1.1.0 fortawesome:fontawesome@4.7.0 -fourseven:scss@4.5.4 +fourseven:scss@4.14.1 gadicc:blaze-react-component@1.4.0 gadicohen:robots-txt@0.0.10 gadicohen:sitemaps@0.0.26 geojson-utils@1.0.10 -github-oauth@1.2.2 -google-oauth@1.2.6 +github-oauth@1.3.0 +google-oauth@1.4.0 hot-code-push@1.0.4 -html-tools@1.0.11 -htmljs@1.0.11 -http@1.4.2 -id-map@1.1.0 -inter-process-messaging@0.1.0 +html-tools@1.1.0 +htmljs@1.1.0 +http@2.0.0 +id-map@1.1.1 +inter-process-messaging@0.1.1 jquery@1.11.11 -lai:collection-extensions@0.2.1_1 -launch-screen@1.1.1 -less@2.7.12 -livedata@1.0.18 +launch-screen@1.3.0 localstorage@1.2.0 -logging@1.1.20 -markdown@1.0.12 -maximum:package-base@1.1.2 -maximum:server-transform@0.5.0 +logging@1.2.0 mdg:geolocation@1.3.0 meteor@1.9.3 -meteor-base@1.4.0 -meteorhacks:inject-initial@1.0.4 -meteorhacks:zones@1.6.0 -meteortesting:browser-tests@0.2.0 -meteortesting:mocha@0.5.1 -minifier-css@1.4.3 -minifier-js@2.5.1 -minimongo@1.4.5 -mobile-experience@1.0.5 -mobile-status-bar@1.0.14 +meteor-base@1.5.0 +minifier-css@1.5.4 +minifier-js@2.6.0 +minimongo@1.7.0 +mobile-experience@1.1.0 +mobile-status-bar@1.1.0 modern-browsers@0.1.5 -modules@0.14.0 -modules-runtime@0.11.0 -mongo@1.7.0 -mongo-decimal@0.1.1 +modules@0.16.0 +modules-runtime@0.12.0 +mongo@1.12.0 +mongo-decimal@0.1.2 mongo-dev-server@1.1.0 -mongo-id@1.0.7 +mongo-id@1.0.8 mongo-livedata@1.0.12 mys:fonts@0.0.2 natestrauser:publish-performant-counts@0.1.2 nimble:restivus@0.8.12 -npm-bcrypt@0.9.3 -npm-mongo@3.2.0 +npm-mongo@3.9.0 nspangler:autoreconnect@0.0.1 -oauth@1.2.8 -oauth2@1.2.1 +oauth@2.0.0 +oauth2@1.3.1 observe-sequence@1.0.16 ordered-dict@1.1.0 -ostrio:cookies@2.1.3 +ostrio:cookies@2.7.0 ostrio:mailer@2.1.0 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 -practicalmeteor:chai@2.1.0_1 -practicalmeteor:mocha-core@1.0.1 -promise@0.11.2 +promise@0.12.0 raix:eventemitter@0.1.3 -random@1.1.0 +random@1.2.0 rate-limit@1.0.9 +react-fast-refresh@0.1.1 react-meteor-data@0.2.16 reactive-dict@1.3.0 reactive-var@1.0.11 -reload@1.3.0 +reload@1.3.1 retry@1.1.0 reywood:publish-composite@1.6.0 routepolicy@1.1.0 saucecode:timezoned-synced-cron@1.2.11 selaias:cookie-consent@0.4.0 -service-configuration@1.0.11 +service-configuration@1.1.0 session@1.2.0 sha@1.0.9 -shell-server@0.4.0 -simple:json-routes@2.1.0 -socket-stream-client@0.2.2 -spacebars@1.0.15 -spacebars-compiler@1.1.3 -srp@1.0.12 -standard-minifier-css@1.5.4 -standard-minifier-js@2.5.2 +shell-server@0.5.0 +simple:json-routes@1.0.4 +socket-stream-client@0.4.0 +spacebars@1.2.0 +spacebars-compiler@1.2.0 +standard-minifier-css@1.7.2 +standard-minifier-js@2.6.0 static-html@1.2.2 -templating@1.3.2 -templating-compiler@1.3.3 -templating-runtime@1.3.2 -templating-tools@1.1.2 +templating@1.4.1 +templating-compiler@1.4.1 +templating-runtime@1.5.0 +templating-tools@1.2.0 themeteorchef:bert@2.1.3 tmeasday:check-npm-versions@0.3.2 tracker@1.2.0 -ui@1.0.13 underscore@1.0.10 -url@1.2.0 +url@1.3.2 vjrj:piwik@0.3.1 -webapp@1.7.5 -webapp-hashing@1.0.9 -xolvio:cleaner@0.3.3 +webapp@1.11.0 +webapp-hashing@1.1.0 diff --git a/imports/api/Comments/Comments.js b/imports/api/Comments/Comments.js new file mode 100644 index 0000000..a45a80f --- /dev/null +++ b/imports/api/Comments/Comments.js @@ -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-', 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; diff --git a/imports/api/Comments/mediaAnalyzers.js b/imports/api/Comments/mediaAnalyzers.js new file mode 100644 index 0000000..7b7a10b --- /dev/null +++ b/imports/api/Comments/mediaAnalyzers.js @@ -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 {}; +} diff --git a/imports/api/Comments/server/index.js b/imports/api/Comments/server/index.js new file mode 100644 index 0000000..40b4368 --- /dev/null +++ b/imports/api/Comments/server/index.js @@ -0,0 +1,3 @@ +// Server-side registration for the Comments feature. +import './methods'; +import './publications'; diff --git a/imports/api/Comments/server/methods.js b/imports/api/Comments/server/methods.js new file mode 100644 index 0000000..49ea3f0 --- /dev/null +++ b/imports/api/Comments/server/methods.js @@ -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 +}); diff --git a/imports/api/Comments/server/onCommentAdd.js b/imports/api/Comments/server/onCommentAdd.js new file mode 100644 index 0000000..f0349c8 --- /dev/null +++ b/imports/api/Comments/server/onCommentAdd.js @@ -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}`); + }); +} diff --git a/imports/api/Comments/server/publications.js b/imports/api/Comments/server/publications.js new file mode 100644 index 0000000..dbd2b0b --- /dev/null +++ b/imports/api/Comments/server/publications.js @@ -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 } } + ); +}); diff --git a/imports/api/FalsePositives/server/publications.js b/imports/api/FalsePositives/server/publications.js index a79ae1a..b6ca42b 100644 --- a/imports/api/FalsePositives/server/publications.js +++ b/imports/api/FalsePositives/server/publications.js @@ -84,7 +84,7 @@ const find = (collection, northEastLng, northEastLat, southWestLng, southWestLat 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 check(northEastLng, NumberBetween(-180, 180)); check(southWestLat, NumberBetween(-90, 90)); @@ -94,7 +94,7 @@ Meteor.publishTransformed('falsePositivesMyloc', function falsePositivesInMyLoc( 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 check(northEastLng, NumberBetween(-180, 180)); check(southWestLat, NumberBetween(-90, 90)); diff --git a/imports/startup/client/comments.js b/imports/startup/client/comments.js deleted file mode 100644 index 94cbe90..0000000 --- a/imports/startup/client/comments.js +++ /dev/null @@ -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 - }); -}); diff --git a/imports/startup/client/comments.scss b/imports/startup/client/comments.scss deleted file mode 100644 index de93e65..0000000 --- a/imports/startup/client/comments.scss +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/imports/startup/common/comments.js b/imports/startup/common/comments.js deleted file mode 100644 index 7b9ab18..0000000 --- a/imports/startup/common/comments.js +++ /dev/null @@ -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 : ''; - } -}); diff --git a/imports/startup/server/comments.js b/imports/startup/server/comments.js deleted file mode 100644 index b010405..0000000 --- a/imports/startup/server/comments.js +++ /dev/null @@ -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'); - } - } - }); -}); diff --git a/imports/startup/server/index.js b/imports/startup/server/index.js index f6a6da8..52d2b81 100644 --- a/imports/startup/server/index.js +++ b/imports/startup/server/index.js @@ -6,10 +6,10 @@ import './accounts'; import './api'; import './fixtures'; import './email'; +import '/imports/api/Comments/server'; import './IPGeocoder'; import './migrations'; import './facts'; -import './comments'; import './sitemaps'; import './subsUnion'; import './prerender'; diff --git a/imports/startup/server/migrations.js b/imports/startup/server/migrations.js index e3bfac9..746a894 100644 --- a/imports/startup/server/migrations.js +++ b/imports/startup/server/migrations.js @@ -1,6 +1,7 @@ -/* global Migrations, Comments */ +/* global Migrations */ /* eslint-disable import/no-absolute-path */ import { Meteor } from 'meteor/meteor'; +import Comments from '/imports/api/Comments/Comments'; import { Accounts } from 'meteor/accounts-base'; import randomHex from 'crypto-random-hex'; import UserSubsToFiresCollection from '/imports/api/Subscriptions/Subscriptions'; @@ -163,7 +164,7 @@ Meteor.startup(() => { Migrations.add({ version: 11, up: function noAnonComments() { - Comments.getCollection().remove({ isAnonymous: true }); + Comments.remove({ isAnonymous: true }); } }); diff --git a/imports/startup/server/sitemaps.js b/imports/startup/server/sitemaps.js index 8026449..52d6008 100644 --- a/imports/startup/server/sitemaps.js +++ b/imports/startup/server/sitemaps.js @@ -1,8 +1,8 @@ -/* global sitemaps Comments */ +/* global sitemaps */ /* eslint-disable import/no-absolute-path */ import Fires from '/imports/api/Fires/Fires'; -// import Comments from '/imports/api/Comments/Comments'; +import Comments from '/imports/api/Comments/Comments'; const firesMapEnabled = false; @@ -32,7 +32,7 @@ sitemaps.add('/sitemap.xml', () => { if (firesMapEnabled) { Fires.find({}, { limit: 100, sort: { createdAt: -1 } }).fetch().forEach((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({ page: `/fire/archive/${fire._id._str}`, lastmod: lastComment ? lastComment.createdAt : fire.updatedAt diff --git a/imports/ui/components/Comments/Comments.scss b/imports/ui/components/Comments/Comments.scss new file mode 100644 index 0000000..c84305b --- /dev/null +++ b/imports/ui/components/Comments/Comments.scss @@ -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; +} diff --git a/imports/ui/components/Comments/CommentsBox.js b/imports/ui/components/Comments/CommentsBox.js new file mode 100644 index 0000000..bf005a9 --- /dev/null +++ b/imports/ui/components/Comments/CommentsBox.js @@ -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 ; + } + if (media.type === 'youtube') { + return ( +
+