diff --git a/api/routes/api/email.js b/api/routes/api/email.js index 07e556880e..4dfb55b84e 100644 --- a/api/routes/api/email.js +++ b/api/routes/api/email.js @@ -61,6 +61,7 @@ emailRouter.get('/unsubscribe', (req, res) => { case 'newThreadCreated': case 'newMessageInThreads': case 'newDirectMessage': + case 'newMention': return unsubscribeUserFromEmailNotification(userId, type).then(() => res .status(200) diff --git a/athena/queues/mention-notification.js b/athena/queues/mention-notification.js index 6a5e8e8044..9afa6416b5 100644 --- a/athena/queues/mention-notification.js +++ b/athena/queues/mention-notification.js @@ -46,10 +46,10 @@ export default async ({ data }: Job) => { // in a private channel where the user is not a member. Users can still be // mentioned in public channels where they are not a member const thread = await getThreadById(threadId); - // if for some reason no thread was found, or the thread was deleted // dont send any notification about the mention if (!thread || thread.deletedAt) return; + debug('got thread'); const { isPrivate: channelIsPrivate } = await getChannelById( thread.channelId @@ -74,6 +74,7 @@ export default async ({ data }: Job) => { ) { return; } + debug('user is member in community'); // see if a usersThreads record exists. If it does, and notifications are muted, we // should not send an email. If the record doesn't exist, it means the person being @@ -120,8 +121,10 @@ export default async ({ data }: Job) => { ]); // if the user shouldn't get an email, just add an in-app notif - if (!shouldEmail) + if (!shouldEmail) { + debug('recipient doesnt have an email'); return storeUsersNotifications(storedNotification.id, recipient.id); + } // if the mention was in a message, get the data about the message const messagePromise = messageId ? await getMessageById(messageId) : null; diff --git a/athena/queues/private-community-request-approved.js b/athena/queues/private-community-request-approved.js index 0f45635c51..ca197aaa95 100644 --- a/athena/queues/private-community-request-approved.js +++ b/athena/queues/private-community-request-approved.js @@ -6,7 +6,7 @@ import Raven from 'shared/raven'; import { getCommunityById } from '../models/community'; import { storeNotification } from '../models/notification'; import { storeUsersNotifications } from 'shared/db/queries/usersNotifications'; -import { getUsers } from 'shared/db/queries/user'; +import { getUserById } from 'shared/db/queries/user'; import { fetchPayload } from '../utils/payloads'; import isEmail from 'validator/lib/isEmail'; import { sendPrivateCommunityRequestApprovedEmailQueue } from 'shared/bull/queues'; @@ -41,25 +41,26 @@ export default async (job: Job) => { const updatedNotification = await storeNotification(nextNotificationRecord); const community = await getCommunityById(communityId); - const recipients = await getUsers([userId]); - const filteredRecipients = recipients.filter( - user => user && isEmail(user.email) - ); - const usersNotificationPromises = filteredRecipients.map(recipient => - storeUsersNotifications(updatedNotification.id, recipient.id) + const recipient = await getUserById(userId); + + const canSendEmail = recipient && recipient.email && isEmail(recipient.email); + + const notificationPromise = storeUsersNotifications( + updatedNotification.id, + recipient.id ); - const usersEmailPromises = filteredRecipients.map(recipient => + const emailPromise = + canSendEmail && sendPrivateCommunityRequestApprovedEmailQueue.add({ // $FlowIssue recipient, community, - }) - ); + }); return await Promise.all([ - ...usersEmailPromises, // handle emails separately - ...usersNotificationPromises, // update or store usersNotifications in-app + emailPromise, // handle emails separately + notificationPromise, // update or store usersNotifications in-app ]).catch(err => { console.error('❌ Error in job:\n'); console.error(err); diff --git a/package.json b/package.json index 89228d3436..4a01e0b2fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Spectrum", - "version": "2.6.4", + "version": "2.7.3", "license": "BSD-3-Clause", "devDependencies": { "@babel/preset-flow": "^7.0.0", diff --git a/scripts/deploy.js b/scripts/deploy.js index 09a62a136a..268f1b8ce4 100644 --- a/scripts/deploy.js +++ b/scripts/deploy.js @@ -75,9 +75,12 @@ if (servers.indexOf('hyperion') > -1) { stdio: 'inherit', }); console.log('Aliasing to hyperion.workers.spectrum.chat'); - exec(now('alias hyperion.workers.spectrum.chat'), { - stdio: 'inherit', - }); + exec( + now(`alias hyperion.${flags.prod ? 'workers' : 'alpha'}.spectrum.chat`), + { + stdio: 'inherit', + } + ); console.log('Clearing cache'); exec( now( diff --git a/shared/db/db.js b/shared/db/db.js index 9f7ad6d97d..13be06daa7 100644 --- a/shared/db/db.js +++ b/shared/db/db.js @@ -59,9 +59,9 @@ poolMaster.on('queueing', size => { statsd.gauge('db.query_queue.size', size); }); -poolMaster.on('size', size => { - statsd.gauge('db.connections.count', size); -}); +setInterval(() => { + statsd.gauge('db.connections.count', poolMaster.getLength()); +}, 5000); // Exit the process on unhealthy db in test env if (process.env.TEST_DB) { diff --git a/shared/imgix/signCommunity.js b/shared/imgix/signCommunity.js index 4fe7b01f04..3583647270 100644 --- a/shared/imgix/signCommunity.js +++ b/shared/imgix/signCommunity.js @@ -8,7 +8,19 @@ export const signCommunity = (community: DBCommunity, expires?: number): DBCommu return { ...rest, - profilePhoto: signImageUrl(profilePhoto, { w: 256, h: 256, expires }), - coverPhoto: signImageUrl(coverPhoto, { w: 1280, h: 384, expires }), + profilePhoto: signImageUrl(profilePhoto, { + w: 256, + h: 256, + dpr: 2, + auto: 'compress', + expires + }), + coverPhoto: signImageUrl(coverPhoto, { + w: 1280, + h: 384, + dpr: 2, + q: 100, + expires + }), }; }; diff --git a/shared/imgix/signThread.js b/shared/imgix/signThread.js index db29bc181b..7123b62d21 100644 --- a/shared/imgix/signThread.js +++ b/shared/imgix/signThread.js @@ -1,6 +1,7 @@ // @flow import type { DBThread } from 'shared/types'; import { signImageUrl } from 'shared/imgix'; +const url = require('url'); const signBody = (body?: string, expires?: number): string => { if (!body) { @@ -32,7 +33,19 @@ const signBody = (body?: string, expires?: number): string => { const { src } = returnBody.entityMap[key].data; // transform the body inline with signed image urls - returnBody.entityMap[key].data.src = signImageUrl(src, { expires }); + const imageUrlStoredAsSigned = + src.indexOf('https://spectrum.imgix.net') >= 0; + // if the image was stored in the db as a signed url (eg. after the plaintext update to the thread editor) + // we need to remove all query params from the src, then re-sign in order to avoid duplicate signatures + // or sending down a url with an expired signature + if (imageUrlStoredAsSigned) { + const pathname = url.parse(src).pathname; + // always attempt to use the parsed pathname, but fall back to the original src + const sanitized = decodeURIComponent(pathname || src); + returnBody.entityMap[key].data.src = signImageUrl(sanitized, { expires }); + } else { + returnBody.entityMap[key].data.src = signImageUrl(src, { expires }); + } }); return JSON.stringify(returnBody); diff --git a/shared/imgix/signUser.js b/shared/imgix/signUser.js index 06e76e92ff..f27b0350df 100644 --- a/shared/imgix/signUser.js +++ b/shared/imgix/signUser.js @@ -10,11 +10,15 @@ export const signUser = (user: DBUser, expires?: number): DBUser => { profilePhoto: signImageUrl(profilePhoto, { w: 256, h: 256, + dpr: 2, + auto: 'compress', expires, }), coverPhoto: signImageUrl(coverPhoto, { w: 1280, h: 384, + dpr: 2, + q: 100, expires, }), }; diff --git a/src/components/loading/style.js b/src/components/loading/style.js index f319dba500..fea96e09bf 100644 --- a/src/components/loading/style.js +++ b/src/components/loading/style.js @@ -300,8 +300,7 @@ export const GridProfile = styled.div` grid-template-areas: 'cover cover' 'meta content'; grid-column-gap: 32px; width: 100%; - min-width: 100%; - max-width: 100%; + max-width: 1280px; height: 100%; min-height: 100vh; background-color: ${theme.bg.default}; diff --git a/src/components/outsideClickHandler/index.js b/src/components/outsideClickHandler/index.js index 0c61ea68b4..beffba65ab 100644 --- a/src/components/outsideClickHandler/index.js +++ b/src/components/outsideClickHandler/index.js @@ -10,14 +10,19 @@ type Props = { class OutsideAlerter extends React.Component { wrapperRef: React.Node; + // iOS bug, see: https://stackoverflow.com/questions/10165141/jquery-on-and-delegate-doesnt-work-on-ipad componentDidMount() { // $FlowFixMe - document.addEventListener('mousedown', this.handleClickOutside); + document + .getElementById('root') + .addEventListener('mousedown', this.handleClickOutside); } componentWillUnmount() { // $FlowFixMe - document.removeEventListener('mousedown', this.handleClickOutside); + document + .getElementById('root') + .removeEventListener('mousedown', this.handleClickOutside); } setWrapperRef = (node: React.Node) => { diff --git a/src/components/upsell/index.js b/src/components/upsell/index.js index 69f1367c5f..ec97d73f1c 100644 --- a/src/components/upsell/index.js +++ b/src/components/upsell/index.js @@ -102,7 +102,7 @@ export const UpsellCreateCommunity = () => { {subtitle} - + diff --git a/src/reset.css.js b/src/reset.css.js index 7d0d223501..14495c2638 100644 --- a/src/reset.css.js +++ b/src/reset.css.js @@ -30,6 +30,7 @@ injectGlobal` padding: 0; margin: 0; -webkit-font-smoothing: antialiased; + -webkit-tap-highlight-color: rgba(0,0,0,0); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; } diff --git a/src/views/channel/style.js b/src/views/channel/style.js index 7a3b92ff42..8c9ad4fba6 100644 --- a/src/views/channel/style.js +++ b/src/views/channel/style.js @@ -14,10 +14,11 @@ export const Grid = styled.main` grid-template-areas: 'cover cover cover' 'meta content extras'; grid-column-gap: 32px; width: 100%; - min-width: 100%; - max-width: 100%; + max-width: 1280px; min-height: 100vh; background-color: ${theme.bg.default}; + box-shadow: inset 1px 0 0 ${theme.bg.border}, + inset -1px 0 0 ${theme.bg.border}; @media (max-width: 1280px) { grid-template-columns: 240px 1fr; diff --git a/src/views/community/style.js b/src/views/community/style.js index 6ea8d77a3c..38a08bb8a2 100644 --- a/src/views/community/style.js +++ b/src/views/community/style.js @@ -130,10 +130,11 @@ export const Grid = styled.main` grid-template-areas: 'cover cover cover' 'meta content extras'; grid-column-gap: 32px; width: 100%; - min-width: 100%; - max-width: 100%; + max-width: 1280px; min-height: 100vh; background-color: ${theme.bg.default}; + box-shadow: inset 1px 0 0 ${theme.bg.border}, + inset -1px 0 0 ${theme.bg.border}; @media (max-width: 1280px) { grid-template-columns: 320px 1fr; diff --git a/src/views/user/style.js b/src/views/user/style.js index f424e34034..6bcbcd365e 100644 --- a/src/views/user/style.js +++ b/src/views/user/style.js @@ -83,10 +83,11 @@ export const Grid = styled.main` grid-template-areas: 'cover cover cover' 'meta content extras'; grid-column-gap: 32px; width: 100%; - min-width: 100%; - max-width: 100%; + max-width: 1280px; min-height: 100vh; background-color: ${theme.bg.default}; + box-shadow: inset 1px 0 0 ${theme.bg.border}, + inset -1px 0 0 ${theme.bg.border}; @media (max-width: 1028px) { grid-template-columns: 320px 1fr;