Merge pull request #2521 from overleaf/ta-announcement-remove

Remove Announcements

GitOrigin-RevId: 3fed602e89992ad5f4260134b87ade1e6e088cf7
This commit is contained in:
Simon Detheridge 2020-01-16 14:54:01 +00:00 committed by Copybot
parent 294418b4ee
commit db27215760
13 changed files with 3 additions and 783 deletions

View file

@ -1,6 +1,4 @@
const settings = require('settings-sharelatex')
const logger = require('logger-sharelatex')
const request = require('request')
const FaultTolerantRequest = require('../../infrastructure/FaultTolerantRequest')
const Errors = require('../Errors/Errors')
@ -61,18 +59,6 @@ const makeAnalyticsBackgroundRequest = function(userId, options, callback) {
FaultTolerantRequest.backgroundRequest(options, callback)
}
// make synchronous request to analytics without retries after checking and
// preparing it.
const makeAnalyticsRequest = function(userId, options, callback) {
let { error, skip } = checkAnalyticsRequest(userId)
if (error || skip) {
return callback(error)
}
prepareAnalyticsRequest(options)
request(options, callback)
}
module.exports = {
identifyUser(userId, oldUserId, callback) {
if (!callback) {
@ -138,25 +124,5 @@ module.exports = {
}
makeAnalyticsBackgroundRequest(userId, opts, callback)
},
getLastOccurrence(userId, event, callback) {
const opts = {
body: {
event
},
json: true,
method: 'POST',
timeout: 1000,
url: `/user/${userId}/event/last_occurrence`
}
makeAnalyticsRequest(userId, opts, function(err, response, body) {
if (err != null) {
logger.warn({ userId, err }, 'error getting last occurance of event')
callback(err)
} else {
callback(null, body)
}
})
}
}

View file

@ -1,59 +0,0 @@
/* eslint-disable
max-len,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const AnnouncementsHandler = require('./AnnouncementsHandler')
const AuthenticationController = require('../Authentication/AuthenticationController')
const logger = require('logger-sharelatex')
const settings = require('settings-sharelatex')
module.exports = {
getUndreadAnnouncements(req, res, next) {
if (
__guard__(
__guard__(
settings != null ? settings.apis : undefined,
x1 => x1.analytics
),
x => x.url
) == null ||
settings.apis.blog.url == null
) {
return res.json([])
}
const user = AuthenticationController.getSessionUser(req)
logger.log(
{ user_id: user != null ? user._id : undefined },
'getting unread announcements'
)
return AnnouncementsHandler.getUnreadAnnouncements(user, function(
err,
announcements
) {
if (err != null) {
logger.warn(
{ err, user_id: user._id },
'unable to get unread announcements'
)
return next(err)
} else {
return res.json(announcements)
}
})
}
}
function __guard__(value, transform) {
return typeof value !== 'undefined' && value !== null
? transform(value)
: undefined
}

View file

@ -1,126 +0,0 @@
/* eslint-disable
handle-callback-err,
max-len,
standard/no-callback-literal,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
let AnnouncementsHandler
const AnalyticsManager = require('../Analytics/AnalyticsManager')
const BlogHandler = require('../Blog/BlogHandler')
const logger = require('logger-sharelatex')
const settings = require('settings-sharelatex')
const async = require('async')
const _ = require('lodash')
module.exports = AnnouncementsHandler = {
_domainSpecificAnnouncements(email) {
const domainSpecific = _.filter(
settings != null ? settings.domainAnnouncements : undefined,
function(domainAnnouncment) {
const matches = _.filter(
domainAnnouncment.domains,
domain => email.indexOf(domain) !== -1
)
return matches.length > 0 && domainAnnouncment.id != null
}
)
return domainSpecific || []
},
getUnreadAnnouncements(user, callback) {
if (callback == null) {
callback = function(err, announcements) {}
}
if (user == null && user._id == null) {
return callback(new Error('user not supplied'))
}
const timestamp = user._id.toString().substring(0, 8)
const userSignupDate = new Date(parseInt(timestamp, 16) * 1000)
return async.parallel(
{
lastEvent(cb) {
return AnalyticsManager.getLastOccurrence(
user._id,
'announcement-alert-dismissed',
cb
)
},
announcements(cb) {
return BlogHandler.getLatestAnnouncements(cb)
}
},
function(err, results) {
if (err != null) {
logger.warn(
{ err, user_id: user._id },
'error getting unread announcements'
)
return callback(err)
}
let domainSpecific = AnnouncementsHandler._domainSpecificAnnouncements(
user != null ? user.email : undefined
)
domainSpecific = _.map(domainSpecific, function(domainAnnouncment) {
try {
domainAnnouncment.date = new Date(domainAnnouncment.date)
return domainAnnouncment
} catch (e) {
return callback(e)
}
})
let { announcements } = results
announcements = _.union(announcements, domainSpecific)
announcements = _.sortBy(announcements, 'date').reverse()
const lastSeenBlogId = __guard__(
__guard__(
results != null ? results.lastEvent : undefined,
x1 => x1.segmentation
),
x => x.blogPostId
)
const announcementIndex = _.findIndex(
announcements,
announcement => announcement.id === lastSeenBlogId
)
announcements = _.map(announcements, function(announcement, index) {
let read
if (announcement.date < userSignupDate) {
read = true
} else if (announcementIndex === -1) {
read = false
} else if (index >= announcementIndex) {
read = true
} else {
read = false
}
announcement.read = read
return announcement
})
return callback(null, announcements)
}
)
}
}
function __guard__(value, transform) {
return typeof value !== 'undefined' && value !== null
? transform(value)
: undefined
}

View file

@ -1,42 +0,0 @@
/* eslint-disable
max-len,
no-unused-vars,
standard/no-callback-literal,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
let BlogHandler
const request = require('request')
const settings = require('settings-sharelatex')
const _ = require('underscore')
const logger = require('logger-sharelatex')
module.exports = BlogHandler = {
getLatestAnnouncements(callback) {
const blogUrl = `${settings.apis.blog.url}/blog/latestannouncements.json`
const opts = {
url: blogUrl,
json: true,
timeout: 1000
}
return request.get(opts, function(err, res, announcements) {
if (err != null) {
return callback(err)
}
if (res.statusCode !== 200) {
return callback(new Error('blog announcement returned non 200'))
}
announcements = _.map(announcements, function(announcement) {
announcement.date = new Date(announcement.date)
return announcement
})
return callback(err, announcements)
})
}
}

View file

@ -323,8 +323,7 @@ module.exports = function(webRouter, privateApiRouter, publicApiRouter) {
chatMessageBgSaturation: '85%',
chatMessageBgLightness: '40%',
defaultFontFamily: 'lucida',
defaultLineHeight: 'normal',
renderAnnouncements: false
defaultLineHeight: 'normal'
}
next()
})

View file

@ -42,7 +42,6 @@ const BetaProgramController = require('./Features/BetaProgram/BetaProgramControl
const SudoModeController = require('./Features/SudoMode/SudoModeController')
const SudoModeMiddleware = require('./Features/SudoMode/SudoModeMiddleware')
const AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter')
const AnnouncementsController = require('./Features/Announcements/AnnouncementsController')
const MetaController = require('./Features/Metadata/MetaController')
const TokenAccessController = require('./Features/TokenAccess/TokenAccessController')
const Features = require('./infrastructure/Features')
@ -688,12 +687,6 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
NotificationsController.markNotificationAsRead
)
webRouter.get(
'/announcements',
AuthenticationController.requireLogin(),
AnnouncementsController.getUndreadAnnouncements
)
// Deprecated in favour of /internal/project/:project_id but still used by versioning
privateApiRouter.get(
'/project/:project_id/details',

View file

@ -16,46 +16,6 @@ block content
}
};
if uiConfig.renderAnnouncements
.announcements(
ng-controller="AnnouncementsController"
ng-class="{ 'announcements-open': ui.isOpen }"
ng-cloak
)
.announcements-backdrop(
ng-if="ui.isOpen"
ng-click="toggleAnnouncementsUI();"
)
a.announcements-btn(
href
ng-if="announcements.length"
ng-click="toggleAnnouncementsUI();"
ng-class="{ 'announcements-btn-open': ui.isOpen, 'announcements-btn-has-new': ui.newItems }"
)
span.announcements-badge(ng-if="ui.newItems") {{ ui.newItems }}
.announcements-body(
ng-if="ui.isOpen"
)
.announcements-scroller
.announcement(
ng-repeat="announcement in announcements | filter:(ui.newItems ? { read: false } : '') track by announcement.id"
)
h2.announcement-header {{ announcement.title }}
p.announcement-description(ng-bind-html="announcement.excerpt")
.announcement-meta
p.announcement-date {{ announcement.date | date:"longDate" }}
a.announcement-link(
ng-href="{{ announcement.url }}"
ng-click="logAnnouncementClick()",
target="_blank"
) Read more
div.text-center(
ng-if="ui.newItems > 0 && ui.newItems < announcements.length"
)
a.btn.btn-default.btn-sm(
href
ng-click="showAll();"
) Show all
main.content.content-alt.project-list-page(
ng-controller="ProjectPageController"
role="main"
@ -77,7 +37,7 @@ block content
span(aria-hidden="true") &times;
span.sr-only #{translate("close")}
.system-message-content(ng-bind-html="htmlContent")
include ../translations/translation_message
.project-list-content(event-tracking=settings.overleaf ? "loads_v2_dash" : "", onboard=settings.overleaf ? "true" : "", event-tracking-trigger=settings.overleaf ? "load" : "", event-tracking-mb="true", event-segmentation="{location: 'dash', v2_onboard: true}")
@ -90,7 +50,7 @@ block content
.project-list-main.col-md-10.col-xs-9
include ./list/notifications
include ./list/project-list
.project-list-empty.row(ng-if="projects.length === 0")
.project-list-empty-col.col-md-offset-2.col-md-8.col-md-offset-2.col-xs-8.col-xs-offset-2
include ./list/empty-project-list

View file

@ -24,7 +24,6 @@ define([
'main/subscription-dashboard',
'main/new-subscription',
'main/annual-upgrade',
'main/announcements',
'main/register-users',
'main/subscription/team-invite-controller',
'main/contact-us',

View file

@ -1,59 +0,0 @@
/* eslint-disable
camelcase,
max-len,
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
define(['base'], App =>
App.controller('AnnouncementsController', function(
$scope,
$http,
eventTracking,
$window,
_
) {
$scope.announcements = []
$scope.ui = {
isOpen: false,
newItems: 0
}
const refreshAnnouncements = () =>
$http.get('/announcements').then(function(response) {
$scope.announcements = response.data
return ($scope.ui.newItems = _.filter(
$scope.announcements,
announcement => !announcement.read
).length)
})
const markAnnouncementsAsRead = () =>
eventTracking.sendMB('announcement-alert-dismissed', {
blogPostId: $scope.announcements[0].id
})
$scope.logAnnouncementClick = () =>
eventTracking.sendMB('announcement-read-more-clicked', {
blogPostId: $scope.announcements[0].id
})
refreshAnnouncements()
$scope.toggleAnnouncementsUI = function() {
$scope.ui.isOpen = !$scope.ui.isOpen
if (!$scope.ui.isOpen && $scope.ui.newItems) {
$scope.ui.newItems = 0
return markAnnouncementsAsRead()
}
}
return ($scope.showAll = () => ($scope.ui.newItems = 0))
}))

View file

@ -1,7 +1,5 @@
@import './list/v1-import-modal.less';
@announcements-shadow: 0 2px 20px rgba(0, 0, 0, 0.5);
@keyframes pulse {
0% {
opacity: 0.7;
@ -648,147 +646,3 @@ ul.project-list {
margin-left: -100px;
}
}
.announcements {
position: absolute;
bottom: @footer-height;
right: 0;
height: 150px;
width: 100%;
pointer-events: none;
overflow: hidden;
&-open {
top: -100%;
height: auto;
pointer-events: all;
}
}
.announcements-backdrop {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.35);
opacity: 0;
animation: fade-in 0.35s forwards;
z-index: 1;
}
.announcements-btn {
position: absolute;
bottom: -35px;
right: 3%;
width: 80px;
height: 80px;
background: url(/img/brand/lion.svg) no-repeat center/80% transparent;
border-radius: 50%;
box-shadow: none;
z-index: 1;
pointer-events: all;
transition: bottom 0.25s cubic-bezier(0.68, -0.55, 0.265, 1.55),
background 0.25s ease, box-shadow 0.25s ease;
&:hover {
bottom: -25px;
}
&-open,
&-open:hover,
&-has-new,
&-has-new:hover {
background-color: #fff;
box-shadow: @announcements-shadow;
bottom: 30px;
}
}
.announcements-badge {
display: inline-block;
position: absolute;
font-size: 11px;
height: 1.8em;
min-width: 1.8em;
border-radius: 0.9em;
line-height: 1.8;
padding: 0 2px;
top: 1px;
right: 1px;
font-weight: bold;
color: #fff;
background-color: @red;
vertical-align: baseline;
white-space: nowrap;
text-align: center;
z-index: 1;
animation: pulse 1s alternate infinite;
}
.announcements-body {
display: flex;
flex-direction: column;
align-items: stretch;
position: absolute;
right: 3%;
margin-right: 95px;
bottom: 30px;
width: 700px;
max-height: 40%;
min-height: 100px;
background: #fff;
z-index: 1;
box-shadow: @announcements-shadow;
border-radius: @border-radius-base;
animation: fade-in 0.35s forwards;
&::after {
content: '\25b8';
position: absolute;
left: 100%;
bottom: 17px;
width: 30px;
color: #fff;
text-shadow: @announcements-shadow;
font-size: 2em;
overflow: hidden;
text-indent: -7px;
}
}
.announcements-scroller {
padding: @line-height-computed;
flex-grow: 0;
overflow-x: hidden;
overflow-y: auto;
}
.announcement {
margin-bottom: @line-height-computed * 1.5;
&:last-child {
margin-bottom: 0;
}
}
.announcement-header {
.page-header;
margin: 0;
}
.announcement-description {
margin: (@line-height-computed / 4) 0 (@line-height-computed / 2);
}
.announcement-meta {
.clearfix;
font-size: 0.9em;
}
.announcement-date {
float: left;
color: @gray;
margin: 0;
}
.announcement-link {
float: right;
margin: 0;
}

View file

@ -102,17 +102,5 @@ describe('AnalyticsManager', function() {
}
)
})
it('getLastOccurrence', function(done) {
const event = 'fake-event'
this.AnalyticsManager.getLastOccurrence(this.fakeUserId, event, error => {
expect(error).to.not.exist
sinon.assert.calledWithMatch(this.request, {
body: { event },
url: 'analytics.test/user/123abc/event/last_occurrence'
})
done()
})
})
})
})

View file

@ -1,250 +0,0 @@
/* eslint-disable
handle-callback-err,
max-len,
no-return-assign,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const should = require('chai').should()
const SandboxedModule = require('sandboxed-module')
const assert = require('assert')
const path = require('path')
const modulePath = path.join(
__dirname,
'../../../../app/src/Features/Announcements/AnnouncementsHandler'
)
const sinon = require('sinon')
const { expect } = require('chai')
describe('AnnouncementsHandler', function() {
beforeEach(function() {
this.user = {
_id: '3c6afe000000000000000000', // 2002-02-14T00:00:00.000Z
email: 'someone@gmail.com'
}
this.AnalyticsManager = { getLastOccurrence: sinon.stub() }
this.BlogHandler = { getLatestAnnouncements: sinon.stub() }
this.settings = {}
return (this.handler = SandboxedModule.require(modulePath, {
globals: {
console: console
},
requires: {
'../Analytics/AnalyticsManager': this.AnalyticsManager,
'../Blog/BlogHandler': this.BlogHandler,
'settings-sharelatex': this.settings,
'logger-sharelatex': {
log() {}
}
}
}))
})
describe('getUnreadAnnouncements', function() {
beforeEach(function() {
this.stubbedAnnouncements = [
{
date: new Date(1478836800000),
id: '/2016/11/01/introducting-latex-code-checker'
},
{
date: new Date(1308369600000),
id: '/2013/08/02/thesis-series-pt1'
},
{
date: new Date(1108369600000),
id: '/2005/08/04/somethingelse'
},
{
date: new Date(1208369600000),
id: '/2008/04/12/title-date-irrelivant'
}
]
return this.BlogHandler.getLatestAnnouncements.callsArgWith(
0,
null,
this.stubbedAnnouncements
)
})
it('should mark all announcements as read is false', function(done) {
this.AnalyticsManager.getLastOccurrence.callsArgWith(2, null, [])
return this.handler.getUnreadAnnouncements(
this.user,
(err, announcements) => {
announcements[0].read.should.equal(false)
announcements[1].read.should.equal(false)
announcements[2].read.should.equal(false)
announcements[3].read.should.equal(false)
return done()
}
)
})
it('should should be sorted again to ensure correct order', function(done) {
this.AnalyticsManager.getLastOccurrence.callsArgWith(2, null, [])
return this.handler.getUnreadAnnouncements(
this.user,
(err, announcements) => {
announcements[3].should.equal(this.stubbedAnnouncements[2])
announcements[2].should.equal(this.stubbedAnnouncements[3])
announcements[1].should.equal(this.stubbedAnnouncements[1])
announcements[0].should.equal(this.stubbedAnnouncements[0])
return done()
}
)
})
it('should return older ones marked as read as well', function(done) {
this.AnalyticsManager.getLastOccurrence.callsArgWith(2, null, {
segmentation: { blogPostId: '/2008/04/12/title-date-irrelivant' }
})
return this.handler.getUnreadAnnouncements(
this.user,
(err, announcements) => {
announcements[0].id.should.equal(this.stubbedAnnouncements[0].id)
announcements[0].read.should.equal(false)
announcements[1].id.should.equal(this.stubbedAnnouncements[1].id)
announcements[1].read.should.equal(false)
announcements[2].id.should.equal(this.stubbedAnnouncements[3].id)
announcements[2].read.should.equal(true)
announcements[3].id.should.equal(this.stubbedAnnouncements[2].id)
announcements[3].read.should.equal(true)
return done()
}
)
})
it('should return all of them marked as read', function(done) {
this.AnalyticsManager.getLastOccurrence.callsArgWith(2, null, {
segmentation: {
blogPostId: '/2016/11/01/introducting-latex-code-checker'
}
})
return this.handler.getUnreadAnnouncements(
this.user,
(err, announcements) => {
announcements[0].read.should.equal(true)
announcements[1].read.should.equal(true)
announcements[2].read.should.equal(true)
announcements[3].read.should.equal(true)
return done()
}
)
})
it('should return posts older than signup date as read', function(done) {
this.stubbedAnnouncements.push({
date: new Date(978836800000),
id: '/2001/04/12/title-date-irrelivant'
})
this.AnalyticsManager.getLastOccurrence.callsArgWith(2, null, [])
return this.handler.getUnreadAnnouncements(
this.user,
(err, announcements) => {
announcements[0].read.should.equal(false)
announcements[1].read.should.equal(false)
announcements[2].read.should.equal(false)
announcements[3].read.should.equal(false)
announcements[4].read.should.equal(true)
announcements[4].id.should.equal('/2001/04/12/title-date-irrelivant')
return done()
}
)
})
describe('with custom domain announcements', function() {
beforeEach(function() {
this.stubbedDomainSpecificAnn = [
{
domains: ['gmail.com', 'yahoo.edu'],
title: 'some message',
excerpt: 'read this',
url: 'http://www.sharelatex.com/i/somewhere',
id: 'iaaa',
date: new Date(1308369600000).toString()
}
]
return (this.handler._domainSpecificAnnouncements = sinon
.stub()
.returns(this.stubbedDomainSpecificAnn))
})
it('should insert the domain specific in the correct place', function(done) {
this.AnalyticsManager.getLastOccurrence.callsArgWith(2, null, [])
return this.handler.getUnreadAnnouncements(
this.user,
(err, announcements) => {
announcements[4].should.equal(this.stubbedAnnouncements[2])
announcements[3].should.equal(this.stubbedAnnouncements[3])
announcements[2].should.equal(this.stubbedAnnouncements[1])
announcements[1].should.equal(this.stubbedDomainSpecificAnn[0])
announcements[0].should.equal(this.stubbedAnnouncements[0])
return done()
}
)
})
})
describe('_domainSpecificAnnouncements', function() {
beforeEach(function() {
return (this.settings.domainAnnouncements = [
{
domains: ['gmail.com', 'yahoo.edu'],
title: 'some message',
excerpt: 'read this',
url: 'http://www.sharelatex.com/i/somewhere',
id: 'id1',
date: new Date(1308369600000).toString()
},
{
domains: ['gmail.com', 'yahoo.edu'],
title: 'some message',
excerpt: 'read this',
url: 'http://www.sharelatex.com/i/somewhere',
date: new Date(1308369600000).toString()
},
{
domains: ['gmail.com', 'yahoo.edu'],
title: 'some message',
excerpt: 'read this',
url: 'http://www.sharelatex.com/i/somewhere',
id: 'id3',
date: new Date(1308369600000).toString()
}
])
})
it("should filter announcments which don't have an id", function(done) {
const result = this.handler._domainSpecificAnnouncements(
'someone@gmail.com'
)
result.length.should.equal(2)
result[0].id.should.equal('id1')
result[1].id.should.equal('id3')
return done()
})
it('should match on domain', function(done) {
this.settings.domainAnnouncements[2].domains = ['yahoo.com']
const result = this.handler._domainSpecificAnnouncements(
'someone@gmail.com'
)
result.length.should.equal(1)
result[0].id.should.equal('id1')
return done()
})
})
})
})

View file

@ -87,7 +87,6 @@ describe('ProjectController', function() {
this.UserController = {
logout: sinon.stub()
}
this.AnalyticsManager = { getLastOccurrence: sinon.stub() }
this.TokenAccessHandler = {
getRequestToken: sinon.stub().returns(this.token),
protectTokens: sinon.stub()
@ -174,7 +173,6 @@ describe('ProjectController', function() {
'./ProjectDetailsHandler': this.ProjectDetailsHandler,
'../Authentication/AuthenticationController': this
.AuthenticationController,
'../Analytics/AnalyticsManager': this.AnalyticsManager,
'../TokenAccess/TokenAccessHandler': this.TokenAccessHandler,
'../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter,
'../../infrastructure/Modules': this.Modules,
@ -1055,7 +1053,6 @@ describe('ProjectController', function() {
)
this.ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub()
this.InactiveProjectManager.reactivateProjectIfRequired.callsArgWith(1)
this.AnalyticsManager.getLastOccurrence.yields(null, { mock: 'event' })
this.ProjectUpdateHandler.markAsOpened.callsArgWith(1)
})