├── .csscomb.json ├── .gitignore ├── .idea ├── .name ├── FOLD.iml ├── codeStyleSettings.xml ├── encodings.xml ├── inspectionProfiles │ ├── Project_Default.xml │ └── profiles_settings.xml ├── jsLibraryMappings.xml ├── misc.xml ├── modules.xml ├── scopes │ └── scope_settings.xml ├── vcs.xml └── watcherTasks.xml ├── .meteor ├── .finished-upgraders ├── .gitignore ├── .id ├── packages ├── platforms ├── release └── versions ├── FOLD.sublime-project ├── LICENSE ├── README.md ├── app.json ├── client ├── about.html ├── activity-feed.js ├── admin.html ├── admin.js ├── analytics.js ├── compatibility │ ├── _linkify.min.js │ ├── flickr-shorturl.js │ ├── hammer.min.js │ ├── jquery-ui.js │ ├── jquery.amaran.min.js │ ├── jquery.easing.min.js │ ├── jquery.hammer.js │ ├── jquery.htmlClean.js │ ├── jquery.konami.min.js │ ├── jquery.tagsinput.js │ ├── linkify-jquery.min.js │ ├── selectordie.js │ └── soundcloud-api.js ├── create-context-blocks.js ├── create.html ├── create.js ├── errors.html ├── fun.html ├── fun.js ├── helpers.js ├── home.html ├── home.js ├── icons.html ├── lib │ ├── constants.js │ ├── devices.js │ ├── helpers.js │ ├── notifications.js │ └── reload.js ├── login.html ├── login.js ├── main.html ├── main.js ├── positioning.js ├── privacy.html ├── profile.html ├── profile.js ├── read.html ├── recover_password.html ├── recover_password.js ├── reset_password.html ├── reset_password.js ├── search-results.js ├── signup.html ├── styles │ ├── MyFontsWebfontsKit.css │ ├── about.lessimport │ ├── amaran.min.css │ ├── create.lessimport │ ├── home.lessimport │ ├── icons.lessimport │ ├── layout.lessimport │ ├── login.lessimport │ ├── metaview.lessimport │ ├── profile.lessimport │ ├── reset_password.lessimport │ ├── story.lessimport │ ├── styles.less │ └── widgets.lessimport ├── terms.html ├── unsubscribe.html └── unsubscribe.js ├── collections ├── collections.js ├── methods.js ├── user-collections.js └── user-methods.js ├── lib ├── activities.js ├── constants.js ├── helpers.js └── router.js ├── package.json ├── public ├── 2014_Ebola_virus_epidemic_in_West_Africa.png ├── Deceased_per_day_Ebola_2014.png ├── EbolaCycle.png ├── Ebola_Betten_Isolation.jpg ├── Ebola_Virus.jpg ├── alright_sans.woff ├── alright_sans_bold.woff ├── batsmonkeys.jpg ├── cdc_doctor_discards.jpg ├── ebola_isolation_chamber.jpg ├── embedtest.html ├── js │ └── responsive-embed.js ├── nurses_1976.jpg └── webfonts │ ├── 2F7411_0_0.eot │ ├── 2F7411_0_0.ttf │ ├── 2F7411_0_0.woff │ ├── 2F7411_0_0.woff2 │ ├── 2F7411_1_0.eot │ ├── 2F7411_1_0.ttf │ ├── 2F7411_1_0.woff │ ├── 2F7411_1_0.woff2 │ ├── 2F7411_2_0.eot │ ├── 2F7411_2_0.ttf │ ├── 2F7411_2_0.woff │ ├── 2F7411_2_0.woff2 │ ├── 2F7411_3_0.eot │ ├── 2F7411_3_0.ttf │ ├── 2F7411_3_0.woff │ ├── 2F7411_3_0.woff2 │ ├── 2F7411_4_0.eot │ ├── 2F7411_4_0.ttf │ ├── 2F7411_4_0.woff │ ├── 2F7411_4_0.woff2 │ ├── 2F7411_5_0.eot │ ├── 2F7411_5_0.ttf │ ├── 2F7411_5_0.woff │ ├── 2F7411_5_0.woff2 │ ├── 2F7411_6_0.eot │ ├── 2F7411_6_0.ttf │ ├── 2F7411_6_0.woff │ ├── 2F7411_6_0.woff2 │ ├── 2F7411_7_0.eot │ ├── 2F7411_7_0.ttf │ ├── 2F7411_7_0.woff │ ├── 2F7411_7_0.woff2 │ ├── 2F7430_0_0.eot │ ├── 2F7430_0_0.ttf │ ├── 2F7430_0_0.woff │ └── 2F7430_0_0.woff2 ├── reset ├── server ├── _reserved-usernames.js ├── accounts.js ├── activities.js ├── browser-policy.js ├── context-methods.js ├── email.js ├── fanout.js ├── fixtures.js ├── index.js ├── methods.js ├── publications.js ├── search.js ├── settings.js └── user-methods.js └── start /.gitignore: -------------------------------------------------------------------------------- 1 | # demeteorizer 2 | .demeteorized 3 | .build* 4 | .DS_Store 5 | 6 | 7 | *.sublime-workspace 8 | 9 | settings.json 10 | 11 | node_modules/ 12 | 13 | .idea/workspace.xml 14 | 15 | # User-specific stuff: 16 | .idea/workspace.xml 17 | .idea/tasks.xml 18 | .idea/dictionaries 19 | 20 | # Sensitive or high-churn files: 21 | .idea/dataSources.ids 22 | .idea/dataSources.xml 23 | .idea/sqlDataSources.xml 24 | .idea/dynamic.xml 25 | .idea/uiDesigner.xml 26 | 27 | # Gradle: 28 | .idea/gradle.xml 29 | .idea/libraries 30 | 31 | # Mongo Explorer plugin: 32 | .idea/mongoSettings.xml 33 | 34 | ## File-based project format: 35 | *.ipr 36 | *.iws 37 | 38 | ## Plugin-specific files: 39 | 40 | # IntelliJ 41 | out/ 42 | 43 | # mpeltonen/sbt-idea plugin 44 | .idea_modules/ 45 | 46 | # JIRA plugin 47 | atlassian-ide-plugin.xml 48 | 49 | #Mongodump 50 | dump 51 | galaxy-test-settings.json 52 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | FOLD -------------------------------------------------------------------------------- /.idea/FOLD.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/codeStyleSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 37 | 39 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/scopes/scope_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | 1.3.0-split-minifiers-package 14 | 1.3.5-remove-old-dev-bundle-link 15 | 1.4.0-remove-old-dev-bundle-link 16 | 1.4.1-add-shell-server-package 17 | -------------------------------------------------------------------------------- /.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | dev_bundle 2 | local 3 | -------------------------------------------------------------------------------- /.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 1ba46l81sknevr1ylnwp3 8 | -------------------------------------------------------------------------------- /.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # 3 | # 'meteor add' and 'meteor remove' will edit this file for you, 4 | # but you can also edit it by hand. 5 | 6 | less@2.7.8 7 | natestrauser:font-awesome@4.0.3 8 | accounts-password@1.3.3 9 | accounts-twitter@1.1.12 10 | underscore@1.0.10 11 | 12 | reactive-var@1.0.11 13 | http@1.2.10 14 | wizonesolutions:underscore-string 15 | aldeed:autoform 16 | aldeed:collection2 17 | service-configuration@1.0.11 18 | meteorhacks:kadira 19 | gadicohen:robots-txt 20 | facts@1.0.9 21 | chaosbohne:twitter-text 22 | bozhao:link-accounts 23 | mystor:device-detection 24 | email@1.1.18 25 | browser-policy@1.0.9 26 | reactive-dict@1.1.8 27 | reload@1.1.11 28 | meteorhacks:subs-manager 29 | fold:fast-render@2.10.0 30 | force-ssl@1.0.13 31 | fold:iron-router@=1.0.10 32 | meteor-base@1.0.4 33 | mobile-experience@1.0.4 34 | mongo@1.1.14 35 | blaze-html-templates 36 | session@1.1.7 37 | jquery@1.11.10 38 | tracker@1.1.1 39 | logging@1.1.16 40 | random@1.0.10 41 | ejson@1.0.13 42 | spacebars 43 | check@1.2.4 44 | 45 | 46 | lepozepo:cloudinary 47 | meteorhacks:search-source 48 | wylio:mandrill 49 | london:body-class 50 | ecmascript@0.6.1 51 | fastclick@1.0.13 52 | standard-minifier-css@1.3.2 53 | standard-minifier-js@1.2.1 54 | shell-server 55 | -------------------------------------------------------------------------------- /.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.4.2.3 2 | -------------------------------------------------------------------------------- /.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.2.14 2 | accounts-oauth@1.1.15 3 | accounts-password@1.3.3 4 | accounts-twitter@1.1.12 5 | aldeed:autoform@5.8.1 6 | aldeed:collection2@2.10.0 7 | aldeed:collection2-core@1.2.0 8 | aldeed:schema-deny@1.1.0 9 | aldeed:schema-index@1.1.1 10 | aldeed:simple-schema@1.5.3 11 | allow-deny@1.0.5 12 | autoupdate@1.2.11 13 | babel-compiler@6.13.0 14 | babel-runtime@1.0.1 15 | base64@1.0.10 16 | binary-heap@1.0.10 17 | blaze@2.1.9 18 | blaze-html-templates@1.0.5 19 | blaze-tools@1.0.10 20 | boilerplate-generator@1.0.11 21 | bozhao:link-accounts@1.2.9 22 | browser-policy@1.0.9 23 | browser-policy-common@1.0.11 24 | browser-policy-content@1.0.12 25 | browser-policy-framing@1.0.12 26 | caching-compiler@1.1.9 27 | caching-html-compiler@1.0.7 28 | callback-hook@1.0.10 29 | chaosbohne:twitter-text@0.1.2 30 | check@1.2.4 31 | chuangbo:cookie@1.1.0 32 | coffeescript@1.11.1_4 33 | ddp@1.2.5 34 | ddp-client@1.2.9 35 | ddp-common@1.2.8 36 | ddp-rate-limiter@1.0.6 37 | ddp-server@1.2.10 38 | deps@1.0.12 39 | diff-sequence@1.0.7 40 | ecmascript@0.6.1 41 | ecmascript-runtime@0.3.15 42 | ejson@1.0.13 43 | email@1.1.18 44 | facts@1.0.9 45 | fastclick@1.0.13 46 | fold:fast-render@2.10.0 47 | fold:iron-router@1.0.10 48 | force-ssl@1.0.13 49 | gadicohen:robots-txt@0.0.10 50 | geojson-utils@1.0.10 51 | hot-code-push@1.0.4 52 | html-tools@1.0.11 53 | htmljs@1.0.11 54 | http@1.2.10 55 | id-map@1.0.9 56 | iron:controller@1.0.12 57 | iron:core@1.0.11 58 | iron:dynamic-template@1.0.12 59 | iron:layout@1.0.12 60 | iron:location@1.0.11 61 | iron:middleware-stack@1.1.0 62 | iron:url@1.0.11 63 | jquery@1.11.10 64 | launch-screen@1.0.12 65 | lepozepo:cloudinary@4.2.6 66 | less@2.7.8 67 | livedata@1.0.18 68 | localstorage@1.0.12 69 | logging@1.1.16 70 | london:body-class@2.3.0 71 | mdg:validation-error@0.2.0 72 | meteor@1.6.0 73 | meteor-base@1.0.4 74 | meteorhacks:inject-data@1.4.1 75 | meteorhacks:kadira@2.30.3 76 | meteorhacks:meteorx@1.4.1 77 | meteorhacks:picker@1.0.3 78 | meteorhacks:search-source@1.4.2 79 | meteorhacks:subs-manager@1.6.4 80 | minifier-css@1.2.15 81 | minifier-js@1.2.15 82 | minimongo@1.0.19 83 | mobile-experience@1.0.4 84 | mobile-status-bar@1.0.13 85 | modules@0.7.7 86 | modules-runtime@0.7.7 87 | momentjs:moment@2.10.6 88 | mongo@1.1.14 89 | mongo-id@1.0.6 90 | mongo-livedata@1.0.12 91 | mystor:device-detection@0.2.0 92 | natestrauser:font-awesome@4.6.3 93 | npm-bcrypt@0.9.2 94 | npm-mongo@2.2.11_2 95 | oauth@1.1.12 96 | oauth1@1.1.11 97 | observe-sequence@1.0.14 98 | ordered-dict@1.0.9 99 | promise@0.8.8 100 | raix:eventemitter@0.1.3 101 | random@1.0.10 102 | rate-limit@1.0.6 103 | reactive-dict@1.1.8 104 | reactive-var@1.0.11 105 | reload@1.1.11 106 | retry@1.0.9 107 | routepolicy@1.0.12 108 | service-configuration@1.0.11 109 | session@1.1.7 110 | sha@1.0.9 111 | shell-server@0.2.1 112 | spacebars@1.0.13 113 | spacebars-compiler@1.0.13 114 | srp@1.0.10 115 | standard-minifier-css@1.3.2 116 | standard-minifier-js@1.2.1 117 | templating@1.2.15 118 | templating-compiler@1.2.15 119 | templating-runtime@1.2.15 120 | templating-tools@1.0.5 121 | tracker@1.1.1 122 | twitter@1.1.14 123 | ui@1.0.12 124 | underscore@1.0.10 125 | url@1.0.11 126 | webapp@1.3.12 127 | webapp-hashing@1.0.9 128 | wizonesolutions:underscore-string@1.0.0 129 | wylio:mandrill@1.0.1 130 | -------------------------------------------------------------------------------- /FOLD.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": ".", 6 | "file_exclude_patterns": ["*.sublime-workspace"], 7 | "folder_exclude_patterns": [".meteor"] 8 | } 9 | 10 | ], 11 | "settings": 12 | { 13 | "tab_size": 2, 14 | "translate_tabs_to_spaces": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright and License for Modified Work (All code additions and changes beginning Oct 23rd, 2015, commit 7db82c68ba2689bc2fb1111b73a5220e8a90cf7c): 2 | 3 | Copyright (c) 2015 Alexis Hope and Joseph Goldbeck. https://readfold.com 4 | 5 | All rights reserved. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Original License for code and changes prior to Oct 23rd, 2015 (commit e851b8d7d6f5b6fa27f7914aa2081b926ff22685) 14 | 15 | The MIT License 16 | 17 | Copyright (c) 2014 - 2015 Kevin Hu and Alexis Hope. https://readfold.com 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy 20 | of this software and associated documentation files (the "Software"), to deal 21 | in the Software without restriction, including without limitation the rights 22 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 23 | copies of the Software, and to permit persons to whom the Software is 24 | furnished to do so, subject to the following conditions: 25 | 26 | The above copyright notice and this permission notice shall be included in 27 | all copies or substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 35 | THE SOFTWARE. 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FOLD 2 | ============= 3 | 4 | 5 | FOLD is a context creation platform for journalists and storytellers, allowing them to structure and craft complex stories. 6 | 7 | **FOLD is live at [readfold.com](https://readfold.com)** 8 | 9 | If you have bug reports, please file issues [here](https://github.com/readFOLD/FOLD/issues). 10 | If you have feature requests, please post them on [our trello board](https://trello.com/b/ImxWYbBy/fold-roadmap) 11 | 12 | To run the FOLD server, API keys are needed for the various search integrations. They can be put in a settings.json file (along with a few other settings variables) containing the following values. 13 | ``` 14 | { 15 | "VIMEO_API_KEY" // used for vimeo context card integration (https://developer.vimeo.com/) 16 | "VIMEO_API_SECRET" 17 | "VIMEO_ACCESS_TOKEN" 18 | "TWITTER_API_KEY" // used for twitter signup and context card integration (https://apps.twitter.com/) 19 | "TWITTER_API_SECRET" 20 | "GOOGLE_API_SERVER_KEY" // used for youtube context card integration (https://console.developers.google.com/ turn on the YouTube Data API) 21 | "FLICKR_API_KEY" // used for flickr context card integration (https://www.flickr.com/services/api/) 22 | "IMGUR_CLIENT_ID" // used for imgur context card integration (https://api.imgur.com/) 23 | "GIPHY_API_KEY" // (can use their public beta key "dc6zaTOxFJmzC" for development) used for giphy context card integration (https://api.giphy.com/) 24 | "SOUNDCLOUD_CLIENT_ID" // used for soundcloud context card integration (https://developers.soundcloud.com/) 25 | "EMBEDLY_KEY" // used to generate previews for link context cards (http://embed.ly/) 26 | "CLOUDINARY_API_KEY" // allows user to upload their own image for headers and context cards (https://cloudinary.com) 27 | "CLOUDINARY_API_SECRET" 28 | "NEW_USER_ACCESS_PRIORITY" // (1 is a good default) an "access priority" for new users, works with PUBLISH_ACCESS_LEVEL and CREATE_ACCESS_LEVEL below to determine if a user is allowed to create a story or publish 29 | "SMTP_USERNAME" // used for sending emails, for example forgotten password emails. not required otherwise. 30 | "SMTP_API_KEY" 31 | "SMTP_SERVER" 32 | "SMTP_PORT" 33 | "public": { 34 | "GOOGLE_API_CLIENT_KEY" // used for google maps integration (https://console.developers.google.com/ turn on the Maps API) 35 | "GA_TRACKING_KEY" // used for analytics.not required otherwise. (https://segment.com/) 36 | "CLOUDINARY_CLOUD_NAME" // allows user to upload their own image for headers and context cards (https://cloudinary.com) 37 | "PUBLISH_ACCESS_LEVEL" // (99999 is a good default) The maximum access priority a user can have and still be allowed to publish 38 | "CREATE_ACCESS_LEVEL" // (99999 is a good default) The maximum access priority a user can have and still be allowed to create a new story 39 | } 40 | } 41 | ``` 42 | 43 | To start, run: `./start` 44 | To reset the database, run: `./reset` 45 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "stack": "heroku-18" 3 | } 4 | -------------------------------------------------------------------------------- /client/about.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /client/activity-feed.js: -------------------------------------------------------------------------------- 1 | 2 | ActivityItems = new Mongo.Collection(null); 3 | 4 | var activityFeedItemsSub; 5 | 6 | var activityFeedSubs = new SubsManager({ 7 | cacheLimit: 1, 8 | expireIn: 99999 9 | }); 10 | 11 | var subscribeToActivityFeedItems = function(cb){ 12 | if(!activityFeedItemsSub){ 13 | activityFeedItemsSub = activityFeedSubs.subscribe("activityFeedItemsPub", function(){ 14 | if(cb){ 15 | cb(); 16 | } 17 | }) 18 | } else { 19 | if(cb){ 20 | cb(); 21 | } 22 | } 23 | }; 24 | 25 | var loadedActivities; 26 | var loadedActivitiesDep = new Tracker.Dependency(); 27 | 28 | var loadInitialActivities = function(cb) { 29 | if (!loadedActivities) { // only load if haven't loaded 30 | Meteor.call('getActivityFeed', function (err, feedItems) { 31 | if (err) { 32 | throw err 33 | } 34 | 35 | loadedActivities = _.pluck(feedItems, '_id'); 36 | loadedActivitiesDep.changed(); 37 | 38 | _.each(feedItems, function (feedItem) { 39 | ActivityItems.insert(feedItem); 40 | }); 41 | 42 | cb(null, loadedActivities); 43 | }); 44 | } else { 45 | 46 | loadedActivities = ActivityItems.find({}).map(function (a) { 47 | return a._id 48 | }); 49 | loadedActivitiesDep.changed(); 50 | cb(null, loadedActivities) 51 | } 52 | }; 53 | 54 | Template.activity_feed.onCreated(function(){ 55 | this.activityFeedLoading = new ReactiveVar(true); 56 | 57 | loadInitialActivities((err, loadedActivities) => { 58 | this.activityFeedLoading.set(false); 59 | 60 | subscribeToActivityFeedItems(() => { 61 | var query = ActivityFeedItems.find({uId: Meteor.userId()}, {sort:{r: -1}, fields: {'aId' : 1}}); 62 | 63 | if(this.activityFeedObserver){ 64 | this.activityFeedObserver.stop(); 65 | } 66 | this.activityFeedObserver = query.observeChanges({ 67 | added (id, aFI) { 68 | if (!_.contains(loadedActivities, aFI.aId)) { 69 | Meteor.call('getActivityFeed', aFI.aId, (err, feedItems) => { 70 | if (err) { 71 | throw err 72 | } 73 | 74 | loadedActivities.push(aFI.aId); 75 | 76 | _.each(feedItems, function (feedItem) { 77 | ActivityItems.insert(feedItem); 78 | }) 79 | }); 80 | } 81 | } 82 | }) 83 | 84 | }) 85 | }) 86 | }); 87 | 88 | Template.activity_feed.onDestroyed(function(){ 89 | unfreezePageScroll(); 90 | if(this.activityFeedObserver){ 91 | this.activityFeedObserver.stop(); 92 | } 93 | }); 94 | 95 | Template.activity_feed.events({ 96 | 'mouseenter .activity-feed' () { 97 | freezePageScroll(); 98 | }, 99 | 'mouseleave .activity-feed' (){ 100 | unfreezePageScroll(); 101 | } 102 | }); 103 | 104 | var feedLimit = Meteor.Device.isPhone() ? 5 : 50; 105 | 106 | Template.activity_feed.helpers({ 107 | populatedFeedItems (){ 108 | return ActivityItems.find({}, {sort: {published: -1}, limit: feedLimit}); 109 | }, 110 | loading (){ 111 | return Template.instance().activityFeedLoading.get(); 112 | }, 113 | hideContent (){ 114 | loadedActivitiesDep.depend(); 115 | return loadedActivities ? false : true; 116 | } 117 | }); 118 | 119 | Template._activity_feed_content.helpers({ 120 | image (){ 121 | if(this.type === 'Person'){ 122 | return getProfileImage(this.imageId, this.twitterId, 'small'); 123 | } else if (this.type === 'Story'){ 124 | return Story.getHeaderImageUrl(this.imageId, 'small'); 125 | } 126 | }, 127 | imageClass (){ 128 | return this.type.toLowerCase() + '-preview-image'; 129 | }, 130 | objectIsYou (){ 131 | return this.object.id === Meteor.userId(); 132 | }, 133 | includeBaselineActivityFeedContent (){ 134 | return Template.instance().data.activities.count() <= (feedLimit - 3); 135 | }, 136 | activityPlaceholders (){ 137 | if(Meteor.Device.isPhone()){ 138 | return 0; 139 | } 140 | var numPlaceholders; 141 | var numActivities = Template.instance().data.activities.count(); 142 | if (numActivities <= 3 ) { 143 | numPlaceholders = 5 - numActivities; 144 | } else { 145 | numPlaceholders = 0; 146 | } 147 | return _.range(numPlaceholders); 148 | }, 149 | hasButton (){ 150 | return _.contains(['Follow', 'FollowBack'], this.type); 151 | }, 152 | noRightImage (){ 153 | return _.contains(['Share'], this.type); 154 | } 155 | }); 156 | 157 | -------------------------------------------------------------------------------- /client/admin.html: -------------------------------------------------------------------------------- 1 | 20 | 21 | 38 | 39 | 47 | -------------------------------------------------------------------------------- /client/admin.js: -------------------------------------------------------------------------------- 1 | Template.admin.helpers({ 2 | usersWhoLoveStories (){ 3 | return _.sortBy(Meteor.users.find().fetch(), function(e){ return -1 * e.profile.favorites.length }) 4 | }, 5 | emailAddress () { 6 | if (this.emails) { 7 | return this.emails[0].address; 8 | } 9 | }, 10 | twitterHandle () { 11 | if (this.services && this.services.twitter && this.services.twitter.screenName) { 12 | return '@' + this.services.twitter.screenName; 13 | } 14 | } 15 | }); 16 | 17 | Template.admin.events({ 18 | 'keypress .impersonate' (e,t) { 19 | if(enterPress(e)){ 20 | var username = t.$('input.impersonate').val(); 21 | Meteor.call('impersonate', username, function (err, userId) { 22 | if(err){ 23 | notifyError(err) 24 | } else { 25 | Meteor.connection.setUserId(userId); 26 | notifySuccess("Ok you're in! Be very very careful."); 27 | Router.go('/'); 28 | } 29 | }); 30 | } 31 | } 32 | }); 33 | 34 | 35 | Template.read_admin_ui.helpers({ 36 | emailAddress () { 37 | var user = Meteor.users.findOne(this.authorId); 38 | if (user && user.emails) { 39 | return user.emails[0].address; 40 | } 41 | }, 42 | twitterHandle () { 43 | var user = Meteor.users.findOne(this.authorId); 44 | if (user && user.services && user.services.twitter) { 45 | return '@' + user.services.twitter.screenName; 46 | } 47 | } 48 | }); 49 | 50 | Template.admin_recent_drafts.helpers({ 51 | recentDrafts () { 52 | return Stories.find({ 53 | published : false 54 | }, { 55 | sort: { 56 | savedAt: -1 57 | } 58 | } 59 | ); 60 | } 61 | }); 62 | 63 | Template.admin_recent_drafts.events({ 64 | 'click .show-more' (){ 65 | Session.set("adminRecentDraftsMore", Session.get("adminRecentDraftsMore") + 1); 66 | } 67 | }); 68 | 69 | 70 | Template.admin_recent_drafts.onCreated(function(){ 71 | Session.setDefault('adminRecentDraftsMore', 0); 72 | 73 | this.autorun(() => { 74 | this.subscribe("adminRecentDraftsPub", {more: Session.get("adminRecentDraftsMore")}) 75 | }); 76 | }); 77 | 78 | Template.admin_recent_activities.onCreated(function(){ 79 | Session.setDefault('adminRecentActivitiesMore', 0); 80 | 81 | this.autorun(() => { 82 | this.subscribe("adminRecentActivitiesPub", {more: Session.get("adminRecentActivitiesMore")}) 83 | }); 84 | }); 85 | 86 | Template.admin_recent_activities.events({ 87 | 'click .show-more' (){ 88 | Session.set("adminRecentActivitiesMore", Session.get("adminRecentActivitiesMore") + 1); 89 | } 90 | }); 91 | 92 | 93 | Template.admin_recent_activities.helpers({ 94 | activities (){ 95 | return Activities.find({}, {sort: {published: -1}}); 96 | } 97 | }); 98 | -------------------------------------------------------------------------------- /client/analytics.js: -------------------------------------------------------------------------------- 1 | 2 | // Google Analytics Snippet // 3 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 4 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 5 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 6 | })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); 7 | 8 | // End Snippet // 9 | 10 | 11 | // Initiate Google Analytics 12 | ga('create', Meteor.settings["public"].GA_TRACKING_KEY, 'auto'); 13 | 14 | 15 | Router.onRun(function() { 16 | Meteor.setTimeout(() => { 17 | $('meta[property="og:url"]').attr('content', window.location.href); 18 | 19 | ga('send', 'pageview', { 20 | title: this.route.getName(), 21 | location: window.location.href 22 | }); // maybe should be more page info here 23 | }, 100); // this might even be ok when set to 0 24 | 25 | this.next() 26 | }); 27 | 28 | window.trackTiming = function(category, str, time){ // mobile safari doesn't have timing api so those results will not include initial request time 29 | trackEvent(str, { 30 | time: time, 31 | nonInteraction: 1 32 | }); 33 | 34 | ga('send', 'timing', category, str, time); 35 | }; 36 | 37 | var jsLoadTime = Date.now() - startTime; 38 | 39 | if (!window.codeReloaded){ 40 | trackTiming('JS', 'JS Loaded', jsLoadTime); 41 | } 42 | 43 | 44 | Meteor.startup(function() { 45 | 46 | if (!window.codeReloaded) { 47 | var timeTillDOMReady = Date.now() - startTime; 48 | 49 | trackTiming('DOM', 'DOM Ready', timeTillDOMReady); 50 | 51 | Tracker.autorun(function(c) { 52 | // waiting for user subscription to load 53 | if (! Router.current() || ! Router.current().ready()) 54 | return; 55 | 56 | var userId = Meteor.userId(); 57 | if (! userId) 58 | return; 59 | 60 | ga('set', 'userId', userId); 61 | 62 | c.stop(); 63 | }); 64 | } 65 | }); 66 | 67 | // TODO alias user when created 68 | 69 | //Accounts.createUser({ 70 | // email: email, 71 | // password: password, 72 | // profile: { 73 | // name: name 74 | // } 75 | //}, function(error) { 76 | // if (! error) { 77 | // analytics.alias(Meteor.userId()); 78 | // } else { 79 | // alert('Error creating account!\n' + EJSON.stringify(error)); 80 | // } 81 | //}); 82 | -------------------------------------------------------------------------------- /client/compatibility/flickr-shorturl.js: -------------------------------------------------------------------------------- 1 | var intval= function (mixed_var, base) { 2 | // http://kevin.vanzonneveld.net 3 | // + original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 4 | // + improved by: stensi 5 | // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 6 | // + input by: Matteo 7 | // + bugfixed by: Brett Zamir (http://brett-zamir.me) 8 | // * example 1: intval('Kevin van Zonneveld'); 9 | // * returns 1: 0 10 | // * example 2: intval(4.2); 11 | // * returns 2: 4 12 | // * example 3: intval(42, 8); 13 | // * returns 3: 42 14 | // * example 4: intval('09'); 15 | // * returns 4: 9 16 | // * example 5: intval('1e', 16); 17 | // * returns 5: 30 18 | 19 | var tmp; 20 | 21 | var type = typeof( mixed_var ); 22 | 23 | if (type === 'boolean') { 24 | return (mixed_var) ? 1 : 0; 25 | } else if (type === 'string') { 26 | tmp = parseInt(mixed_var, base || 10); 27 | return (isNaN(tmp) || !isFinite(tmp)) ? 0 : tmp; 28 | } else if (type === 'number' && isFinite(mixed_var) ) { 29 | return Math.floor(mixed_var); 30 | } else { 31 | return 0; 32 | } 33 | } 34 | 35 | var base_encode= function(num, alphabet) { 36 | // http://tylersticka.com/ 37 | // Based on the Flickr PHP snippet: 38 | // http://www.flickr.com/groups/api/discuss/72157616713786392/ 39 | alphabet = alphabet || '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ'; 40 | var base_count = alphabet.length; 41 | var encoded = ''; 42 | while (num >= base_count) { 43 | var div = num/base_count; 44 | var mod = (num-(base_count*intval(div))); 45 | encoded = alphabet.charAt(mod) + encoded; 46 | num = intval(div); 47 | } 48 | if (num) encoded = alphabet.charAt(num) + encoded; 49 | return encoded; 50 | } 51 | 52 | var base_decode= function(num, alphabet) { 53 | // http://www.flickr.com/groups/api/discuss/72157616713786392/72157620931323757/ 54 | // Original by Taiyo Fujii 55 | alphabet = alphabet || '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ'; 56 | var len = num.length ; 57 | var decoded = 0 ; 58 | var multi = 1 ; 59 | for (var i = (len-1) ; i >= 0 ; i--) { 60 | decoded = decoded + multi * alphabet.indexOf(num[i]) ; 61 | multi = multi * alphabet.length ; 62 | } 63 | return decoded; 64 | } 65 | 66 | var getLastInUrl= function(url) { 67 | // http://tylersticka.com 68 | // Fetches the last item in a URL, in this case a Flickr ID 69 | url.replace(/^\s+|\s+$/g,""); 70 | if (url.charAt(url.length-1) == '/') { 71 | url = url.substr(0,url.length-1); 72 | } 73 | url = url.split('/'); 74 | return url[url.length-1]; 75 | } 76 | 77 | window.encodeFlickrUrl = function(url) { 78 | // Returns a flic.kr URL from a full flickr.com URL 79 | return 'http://flic.kr/p/' + base_encode(getLastInUrl(url)); 80 | } 81 | -------------------------------------------------------------------------------- /client/compatibility/jquery.amaran.min.js: -------------------------------------------------------------------------------- 1 | (function(){!function(t,i){var e,n;return e=function(i){var e;e={position:"bottom right",content:" ",delay:3e3,sticky:!1,stickyButton:!1,inEffect:"fadeIn",outEffect:"fadeOut",theme:"default",themeTemplate:null,closeOnClick:!0,closeButton:!1,clearAll:!1,cssanimationIn:!1,cssanimationOut:!1,resetTimeout:!1,overlay:!1,beforeStart:function(){},afterEnd:function(){},onClick:function(){},wrapper:".amaran-wrapper"},this.config=t.extend({},e,i),this.config.beforeStart(),this.init(),this.close()},e.prototype={init:function(){var i,e,o,a,s,c,r,h;r=null,h=null,o=this.config.position.split(" "),t(this.config.wrapper).length&&t(this.config.wrapper).hasClass(this.config.position)?(r=t(this.config.wrapper+"."+o[0]+"."+o[1]),s=r.find(".amaran-wrapper-inner")):(r=t("
",{"class":this.config.wrapper.substr(1,this.config.wrapper.length)+" "+this.config.position}).appendTo("body"),s=t("
",{"class":"amaran-wrapper-inner"}).appendTo(r)),"object"==typeof this.config.content?c=null!=this.config.themeTemplate?this.config.themeTemplate(this.config.content):n[this.config.theme.split(" ")[0]+"Theme"](this.config.content):(this.config.content={},this.config.content.message=this.config.message,this.config.content.color="#27ae60",c=n.defaultTheme(this.config.content)),i={"class":this.config.themeTemplate?"amaran "+this.config.content.themeName:this.config.theme&&!this.config.themeTemplate?"amaran "+this.config.theme:"amaran",html:this.buildHTML(c)},this.config.clearAll&&t(".amaran").remove(),a=t("
",i).appendTo(s),"center"===o[0]&&this.centerCalculate(r,s),this.animation(this.config.inEffect,a,"show"),this.config.onClick&&(e=this,t(a).css({cursor:"default"}),t(a).on("click",function(i){return t(i.target).is(".amaran-close")||t(i.target).is(".amaran-sticky")?void i.preventDefault():void e.config.onClick()})),this.config.resetTimeout&&(e=this,t(a).on("mouseenter",function(){return e.resetTimeout()}),t(a).on("mouseleave",function(){return e.resumeTimeout(a)})),this.config.overlay&&t(".amaran-overlay").length<=0&&t("body").prepend('
'),this.config.stickyButton&&(e=this,t(a).find(".amaran-sticky").on("click",function(){return t(this).hasClass("sticky")?(e.resumeTimeout(a),t(this).removeClass("sticky")):(e.resetTimeout(),t(this).addClass("sticky"))})),this.config.sticky!==!0&&this.hideDiv(a)},resetTimeout:function(){var t;return t=this,clearTimeout(t.timeout)},resumeTimeout:function(t){var i;return i=this,i.timeout=setTimeout(function(){return i.animation(i.config.outEffect,t,"hide")},i.config.delay)},buildHTML:function(t){return this.config.closeButton&&(t=''+t),this.config.stickyButton&&(t=''+t),t},centerCalculate:function(t,i){var e,n,o;n=i.find(".amaran").length,o=i.height(),e=(t.height()-o)/2,i.find(".amaran:first-child").animate({"margin-top":e},200)},animation:function(t,i,e){return"fadeIn"===t||"fadeOut"===t?this.fade(i,e):"show"===t?this.cssanimate(i,e):this.slide(t,i,e)},fade:function(t,i){var e;return e=this,"show"===i?this.config.cssanimationIn?t.addClass("animated "+this.config.cssanimationIn).show():t.fadeIn():this.config.cssanimationOut?(t.addClass("animated "+this.config.cssanimationOut),t.css({"min-height":0,height:t.outerHeight()}),void t.animate({opacity:0},function(){t.animate({height:0},function(){e.removeIt(t)})})):(t.css({"min-height":0,height:t.outerHeight()}),void t.animate({opacity:0},function(){t.animate({height:0},function(){e.removeIt(t)})}))},removeIt:function(i){var e,n;clearTimeout(this.timeout),i.remove(),n=t(this.config.wrapper+"."+this.config.position.split(" ")[0]+"."+this.config.position.split(" ")[1]),e=n.find(".amaran-wrapper-inner"),"center"===this.config.position.split(" ")[0]&&this.centerCalculate(n,e),this.config.afterEnd(),this.config.overlay&&0===t(".amaran").length&&t(".amaran-overlay").fadeOut(400,function(){return t(this).remove()})},getWidth:function(t){var i,e;return i=t.clone().hide().appendTo("body"),e=i.outerWidth()+i.outerWidth()/2,i.remove(),e},getInfo:function(i){var e,n;return e=i.offset(),n=t(this.config.wrapper).offset(),{t:e.top,l:e.left,h:i.height(),w:i.outerWidth(),wT:n.top,wL:n.left,wH:t(this.config.wrapper).outerHeight(),wW:t(this.config.wrapper).outerWidth()}},getPosition:function(e,n){var o,a,s;return o=this.getInfo(e),a=this.config.position.split(" ")[1],s={slideTop:{start:{top:-(o.wT+o.wH+2*o.h)},move:{top:0},hide:{top:-(o.t+2*o.h)},height:o.h},slideBottom:{start:{top:t(i).height()-o.wH+2*o.h},move:{top:0},hide:{top:t(i).height()-o.wH+2*o.h},height:o.h},slideLeft:{start:{left:"left"===a?1.5*-o.w:-t(i).width()},move:{left:0},hide:{left:"left"===a?1.5*-o.w:-t(i).width()},height:o.h},slideRight:{start:{left:"right"===a?1.5*o.w:t(i).width()},move:{left:0},hide:{left:"right"===a?1.5*o.w:t(i).width()},height:o.h}},s[n]?s[n]:0},slide:function(t,i,e){var n,o;return o=this.getPosition(i,t),"show"!==e?(n=this,i.animate(o.hide,function(){i.css({"min-height":0,height:o.height},function(){i.html(" ")})}).animate({height:0},function(){return n.removeIt(i)})):void i.show().css(o.start).animate(o.move)},close:function(){var i;return i=this,t("[data-amaran-close]").on("click",function(){i.animation(i.config.outEffect,t(this).closest("div.amaran"),"hide")}),!this.config.closeOnClick&&this.config.closeButton?void i.animation(i.config.outEffect,t(this).parent("div.amaran"),"hide"):void(this.config.closeOnClick&&t(".amaran").on("click",function(){i.animation(i.config.outEffect,t(this),"hide")}))},hideDiv:function(t){var i;i=this,i.timeout=setTimeout(function(){i.animation(i.config.outEffect,t,"hide")},i.config.delay)}},n={defaultTheme:function(t){var i;return i="","undefined"!=typeof t.color&&(i=t.color),"
"+t.message+"
"},awesomeTheme:function(t){return'

'+t.title+"

"+t.message+''+t.info+"

"},userTheme:function(t){return'
'+t.user+""+t.message+"
"},colorfulTheme:function(t){var i,e;return"undefined"!=typeof t.color&&(e=t.color),"undefined"!=typeof t.bgcolor&&(i=t.bgcolor),"
"+t.message+"
"},tumblrTheme:function(t){return'
'+t.title+'
'+t.message+"
"}},t.amaran=function(t){var i;return i=new e(t)},t.amaran.close=function(){return t(".amaran-wrapper").remove(),!1}}(jQuery,window,document)}).call(this); -------------------------------------------------------------------------------- /client/compatibility/jquery.easing.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Easing v1.3 - http://gsgd.co.uk/sandbox/jquery/easing/ 3 | * 4 | * Uses the built in easing capabilities added In jQuery 1.1 5 | * to offer multiple easing options 6 | * 7 | * TERMS OF USE - EASING EQUATIONS 8 | * 9 | * Open source under the BSD License. 10 | * 11 | * Copyright © 2001 Robert Penner 12 | * All rights reserved. 13 | * 14 | * TERMS OF USE - jQuery Easing 15 | * 16 | * Open source under the BSD License. 17 | * 18 | * Copyright © 2008 George McGinley Smith 19 | * All rights reserved. 20 | * 21 | * Redistribution and use in source and binary forms, with or without modification, 22 | * are permitted provided that the following conditions are met: 23 | * 24 | * Redistributions of source code must retain the above copyright notice, this list of 25 | * conditions and the following disclaimer. 26 | * Redistributions in binary form must reproduce the above copyright notice, this list 27 | * of conditions and the following disclaimer in the documentation and/or other materials 28 | * provided with the distribution. 29 | * 30 | * Neither the name of the author nor the names of contributors may be used to endorse 31 | * or promote products derived from this software without specific prior written permission. 32 | * 33 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 34 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 35 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 36 | * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 37 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 38 | * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 39 | * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 40 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 41 | * OF THE POSSIBILITY OF SUCH DAMAGE. 42 | * 43 | */ 44 | jQuery.easing.jswing=jQuery.easing.swing;jQuery.extend(jQuery.easing,{def:"easeOutQuad",swing:function(e,f,a,h,g){return jQuery.easing[jQuery.easing.def](e,f,a,h,g)},easeInQuad:function(e,f,a,h,g){return h*(f/=g)*f+a},easeOutQuad:function(e,f,a,h,g){return -h*(f/=g)*(f-2)+a},easeInOutQuad:function(e,f,a,h,g){if((f/=g/2)<1){return h/2*f*f+a}return -h/2*((--f)*(f-2)-1)+a},easeInCubic:function(e,f,a,h,g){return h*(f/=g)*f*f+a},easeOutCubic:function(e,f,a,h,g){return h*((f=f/g-1)*f*f+1)+a},easeInOutCubic:function(e,f,a,h,g){if((f/=g/2)<1){return h/2*f*f*f+a}return h/2*((f-=2)*f*f+2)+a},easeInQuart:function(e,f,a,h,g){return h*(f/=g)*f*f*f+a},easeOutQuart:function(e,f,a,h,g){return -h*((f=f/g-1)*f*f*f-1)+a},easeInOutQuart:function(e,f,a,h,g){if((f/=g/2)<1){return h/2*f*f*f*f+a}return -h/2*((f-=2)*f*f*f-2)+a},easeInQuint:function(e,f,a,h,g){return h*(f/=g)*f*f*f*f+a},easeOutQuint:function(e,f,a,h,g){return h*((f=f/g-1)*f*f*f*f+1)+a},easeInOutQuint:function(e,f,a,h,g){if((f/=g/2)<1){return h/2*f*f*f*f*f+a}return h/2*((f-=2)*f*f*f*f+2)+a},easeInSine:function(e,f,a,h,g){return -h*Math.cos(f/g*(Math.PI/2))+h+a},easeOutSine:function(e,f,a,h,g){return h*Math.sin(f/g*(Math.PI/2))+a},easeInOutSine:function(e,f,a,h,g){return -h/2*(Math.cos(Math.PI*f/g)-1)+a},easeInExpo:function(e,f,a,h,g){return(f==0)?a:h*Math.pow(2,10*(f/g-1))+a},easeOutExpo:function(e,f,a,h,g){return(f==g)?a+h:h*(-Math.pow(2,-10*f/g)+1)+a},easeInOutExpo:function(e,f,a,h,g){if(f==0){return a}if(f==g){return a+h}if((f/=g/2)<1){return h/2*Math.pow(2,10*(f-1))+a}return h/2*(-Math.pow(2,-10*--f)+2)+a},easeInCirc:function(e,f,a,h,g){return -h*(Math.sqrt(1-(f/=g)*f)-1)+a},easeOutCirc:function(e,f,a,h,g){return h*Math.sqrt(1-(f=f/g-1)*f)+a},easeInOutCirc:function(e,f,a,h,g){if((f/=g/2)<1){return -h/2*(Math.sqrt(1-f*f)-1)+a}return h/2*(Math.sqrt(1-(f-=2)*f)+1)+a},easeInElastic:function(f,h,e,l,k){var i=1.70158;var j=0;var g=l;if(h==0){return e}if((h/=k)==1){return e+l}if(!j){j=k*0.3}if(gi.push(t)||(n.code.length=0;r--)e.insertBefore(n[r],i),i=n[r]}function i(e,t,n){for(var i=[],r=0;r0&&(e.splice(l-1,2),l-=2)}e=e.join("/")}if((n||s)&&i){o=e.split("/");for(l=o.length;l>0;l-=1){u=o.slice(0,l).join("/");if(n)for(c=n.length;c>0;c-=1){a=i[n.slice(0,c).join("/")];if(a){a=a[u];if(a){f=a;break}}}f=f||s[u];if(f){o.splice(0,l,f),e=o.join("/");break}}}return e}function f(t,n){return function(){return u.apply(e,s.call(arguments,0).concat([t,n]))}}function l(e){return function(t){return a(t,e)}}function c(e){return function(n){t[e]=n}}function h(r){if(n.hasOwnProperty(r)){var s=n[r];delete n[r],i[r]=!0,o.apply(e,s)}if(!t.hasOwnProperty(r))throw new Error("No "+r);return t[r]}function p(e,t){var n,r,i=e.indexOf("!");return i!==-1?(n=a(e.slice(0,i),t),e=e.slice(i+1),r=h(n),r&&r.normalize?e=r.normalize(e,l(t)):e=a(e,t)):e=a(e,t),{f:n?n+"!"+e:e,n:e,p:r}}function d(e){return function(){return r&&r.config&&r.config[e]||{}}}var t={},n={},r={},i={},s=[].slice,o,u;o=function(r,s,o,u){var a=[],l,v,m,g,y,b;u=u||r,typeof o=="string"&&(o=__inflate(r,o));if(typeof o=="function"){s=!s.length&&o.length?["require","exports","module"]:s;for(b=0;b-1,s=new p(e),f.push(new d(s,e,i)),s)},v.Events=o,window.SC=window.SC||{},window.SC.Widget=v,d=function(e,t,n){this.instance=e,this.element=t,this.domain=E(t.getAttribute("src")),this.isReady=!!n,this.callbacks={}},p=function(){},p.prototype={constructor:p,load:function(e,t){if(!e)return;t=t||{};var n=this,r=k(this),i=r.element,s=i.src,a=s.substr(0,s.indexOf("?"));r.isReady=!1,r.playEventFired=!1,i.onload=function(){n.bind(o.READY,function(){var e,n=r.callbacks;for(e in n)n.hasOwnProperty(e)&&e!==o.READY&&C(u.ADD_LISTENER,e,r.element);t.callback&&t.callback()})},i.src=M(a,e,t)},bind:function(e,t){var n=this,r=k(this);return r&&r.element&&(e===o.READY&&r.isReady?setTimeout(t,1):r.isReady?(T(e,t,r),C(u.ADD_LISTENER,e,r.element)):T(l,function(){n.bind(e,t)},r)),this},unbind:function(e){var t=k(this),n;t&&t.element&&(n=N(e,t),e!==o.READY&&n&&C(u.REMOVE_LISTENER,e,t.element))}},O(p.prototype,x(i)),O(p.prototype,x(s),!0)}),window.SC=window.SC||{},window.SC.Widget=require("lib/api/api")})() -------------------------------------------------------------------------------- /client/errors.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 19 | 20 | 28 | -------------------------------------------------------------------------------- /client/fun.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /client/fun.js: -------------------------------------------------------------------------------- 1 | var handpickedStories = [ 2 | "/read/riascience/fifty-years-of-walking-in-space-and-what-we-found-there-uRTtQWQo", 3 | "/read/FOLD/how-close-are-we-to-the-martian-Bret9g44", 4 | "/read/BDatta/this-is-not-a-hologram-w5hosSJa", 5 | "/read/twelvefifths/reaction-diffusion-systems-rvJzfQ6k", 6 | "/read/kimsmith/automating-creativity-n5E8qJeF", 7 | "/read/CorySchmitz/how-i-make-textures-kLiQK8se", 8 | "/read/trainbabie/what-is-hipsterdom-nqeiz7XP", 9 | "/read/HannahRajnicek/friday-the-13th-in-chicago-superstitions-tattoo-culture-MoEmXgMM", 10 | "/read/timdunlop/the-first-time-ever-i-saw-your-face-apFj8gt9", 11 | "/read/smwat/dada-data-and-the-internet-of-paternalistic-things-TsZQXLjK", 12 | "/read/SproutsIO/finding-flavor-2Pf475CJ", 13 | "/read/aminobiotech/why-you-should-grow-your-own-bacteria-at-home-JodcMMXB", 14 | "/read/APCollector/on-a-mans-modular-synth-iRaJbfjY", 15 | "/read/EthanZ/choosing-the-appropriate-extreme-metal-music-to-listen-to-while-grading-masters-theses-SESbL2qK", 16 | "/read/CorySchmitz/how-i-make-halftones-nmQRSPe5", 17 | "/read/manuelaristaran/digital-public-services-user-experience-matters-WwpPfdJq", 18 | "/read/sammireinstein/it-is-what-it-is-conversations-about-iraq-WotpdjNa", 19 | "/read/AnnieHuang/shepard-faireys-obey-NnmADS2Z", 20 | "/read/JanineKwoh/why-diversity-matters-in-the-card-aisle-cR9WaHQe", 21 | "/read/cesifoti/three-women-scholars-you-should-know-but-you-probably-dont-evYYD35C", 22 | "/read/Jeremy/proof-of-work-20-He8cm2WC", 23 | "/read/sgenner/why-screens-can-ruin-your-sleep-XuiGfrJi", 24 | "/read/sultanalqassemi/sultan-al-qassemi-on-mit-media-lab-imagination-realized-i8ZS3Dtg", 25 | "/read/MattCarroll/mr-spock-to-the-rescue-how-a-star-trek-star-earned-the-admiration-of-a-young-fan-v5Rr3gGf" 26 | ]; 27 | 28 | var handpickedPeople = [ 29 | "/profile/twelvefifths", 30 | "/profile/FOLD", 31 | "/profile/alexishope", 32 | "/profile/EthanZ", 33 | "/profile/Rochelle", 34 | "/profile/jbobrow", 35 | "/profile/DestinyInFocus", 36 | "/profile/SproutsIO", 37 | "/profile/Cristian_jf", 38 | "/profile/cjaffe", 39 | "/profile/mpetitchou", 40 | "/profile/tor", 41 | "/profile/JanineKwoh", 42 | "/profile/trainbabie", 43 | "/profile/CorySchmitz", 44 | "/profile/MattCarroll", 45 | "/profile/HannahRajnicek", 46 | "/profile/delong", 47 | "/profile/aminobiotech", 48 | "/profile/cesifoti", 49 | "/profile/jovialjoy", 50 | "/profile/shailin", 51 | "/profile/sannabh", 52 | "/profile/sgenner", 53 | "/profile/BDatta", 54 | "/profile/smwat", 55 | "/profile/APCollector", 56 | "/profile/MikeMoschella" 57 | ]; 58 | 59 | 60 | Template.random_story.onCreated(function(){ 61 | this.options = handpickedStories; 62 | }); 63 | 64 | Template.random_person.onCreated(function(){ 65 | this.options = handpickedPeople; 66 | }); 67 | 68 | 69 | _.each(['random_story', 'random_person'], function(templateName){ 70 | Template[templateName].onCreated(function() { 71 | this.randomizedLink = new ReactiveVar(); 72 | this.rolling = new ReactiveVar(); 73 | }); 74 | 75 | Template[templateName].onRendered(function(){ 76 | this.autorun(() => { 77 | var currentUrl = Router.current().url; 78 | this.links = _.reject(this.options, function(url){ 79 | return _s.include(url, idFromPathSegment(currentUrl)); 80 | }); 81 | 82 | this.randomizedLink.set(_.sample(this.links)); 83 | }); 84 | 85 | this.rollTheDice = (cb) => { 86 | this.rolling.set(true); 87 | //var keepRolling = Meteor.setInterval(function(){ 88 | // this.randomizedLink.set(_.sample(this.links)); 89 | //}, 50); 90 | Meteor.setTimeout(() => { 91 | //clearInterval(keepRolling); 92 | this.rolling.set(false); 93 | if(cb){ 94 | cb(); 95 | } 96 | }, 1100); 97 | } 98 | }); 99 | 100 | Template[templateName].helpers({ 101 | rolling (){ 102 | return Template.instance().rolling.get(); 103 | }, 104 | randomizedLink (){ 105 | return Template.instance().randomizedLink.get(); 106 | } 107 | }); 108 | 109 | Template[templateName].events({ 110 | 'click' (e, t){ 111 | e.preventDefault(); 112 | t.rollTheDice(function(){ 113 | Router.go(t.randomizedLink.get()); 114 | }) 115 | trackEvent('Click random story button'); 116 | } 117 | }); 118 | }) 119 | 120 | 121 | -------------------------------------------------------------------------------- /client/helpers.js: -------------------------------------------------------------------------------- 1 | Handlebars.registerHelper("debugContext", function() { 2 | return console.log(this); 3 | }); 4 | 5 | Handlebars.registerHelper("log", function(v) { 6 | return console.log(v); 7 | }); 8 | 9 | Handlebars.registerHelper("hasContext", function(v) { 10 | return !_.isEmpty(this); 11 | }); 12 | 13 | Handlebars.registerHelper("pastHeader", function() { 14 | return Session.get("pastHeader"); 15 | }); 16 | 17 | Handlebars.registerHelper("read", function() { 18 | return Session.get("read"); 19 | }); 20 | 21 | Handlebars.registerHelper("notRead", function() { 22 | return !Session.get("read"); 23 | }); 24 | 25 | Handlebars.registerHelper("showPublished", function() { 26 | return !Session.get("showDraft"); 27 | }); 28 | 29 | Handlebars.registerHelper("showDraft", function() { 30 | return Session.get("showDraft"); 31 | }); 32 | 33 | Handlebars.registerHelper("saving", function() { 34 | return Session.get("saving"); 35 | }); 36 | 37 | Handlebars.registerHelper("signingIn", function() { 38 | return window.signingIn(); 39 | }); 40 | 41 | Handlebars.registerHelper("currentXReadableIndex", function() { 42 | return Session.get("currentX") + 1; 43 | }); 44 | 45 | Handlebars.registerHelper("currentYId", function() { 46 | return Session.get("currentYId"); 47 | }); 48 | 49 | Handlebars.registerHelper("addingContext", function() { 50 | return Session.get("addingContext"); 51 | }); 52 | 53 | Handlebars.registerHelper("editingThisContext", function() { 54 | var editingContext = Session.get("editingContext"); 55 | if (editingContext){ 56 | return editingContext === this._id; 57 | } 58 | }); 59 | 60 | Handlebars.registerHelper("UsersCollection", Meteor.users); 61 | 62 | Handlebars.registerHelper("isAuthor", function() { 63 | var userId = Meteor.userId(); 64 | return userId && userId === this.authorId; 65 | }); 66 | 67 | Handlebars.registerHelper("isAuthorOrAdmin", function() { 68 | var userId = Meteor.userId(); 69 | if (userId && userId === this.authorId){ 70 | return true 71 | } else { 72 | var user = Meteor.user(); 73 | return user && user.admin; 74 | } 75 | }); 76 | 77 | Handlebars.registerHelper("cardWidth", function() { 78 | return Session.get("cardWidth"); 79 | }); 80 | 81 | Handlebars.registerHelper("cardHeight", function() { // for context cards, particularly in mobile 82 | return Session.get("cardHeight"); 83 | }); 84 | 85 | Handlebars.registerHelper("windowWidth", function() { 86 | return Session.get("windowWidth"); 87 | }); 88 | 89 | Handlebars.registerHelper("windowHeight", function() { 90 | return Session.get("windowHeight"); 91 | }); 92 | 93 | Handlebars.registerHelper("verticalLeft", function() { 94 | return getVerticalLeft(); 95 | }); 96 | 97 | Handlebars.registerHelper("adminMode", function() { 98 | return adminMode(); 99 | }); 100 | 101 | Handlebars.registerHelper("audioPopoutExists", function() { 102 | return Session.equals('poppedOutContextType', 'audio'); 103 | }); 104 | 105 | Handlebars.registerHelper("videoPopoutExists", function() { 106 | return Session.equals('poppedOutContextType', 'video'); 107 | }); 108 | 109 | Handlebars.registerHelper("reactiveStory", function(){ 110 | return Stories.findOne(Session.get('storyId')); 111 | }); 112 | 113 | Handlebars.registerHelper("twitterUser", function() { 114 | var user = Meteor.user(); 115 | return user && user.services && user.services.twitter && user.services.twitter.id; 116 | }); 117 | 118 | Handlebars.registerHelper("firstName", function(user) { 119 | if (user && user.profile) { 120 | return user.profile.name.split(' ')[0]; 121 | } 122 | }); 123 | 124 | Handlebars.registerHelper("userFavorited", function() { 125 | return Meteor.user() && _.contains(Meteor.user().profile.favorites, this._id); 126 | }); 127 | 128 | Handlebars.registerHelper("userFollowing", function(id) { 129 | return id === Meteor.userId() || Meteor.user() && _.contains(Meteor.user().profile.following, id); 130 | }); 131 | 132 | Handlebars.registerHelper("showStorySandwichFooter", function () { 133 | return !Meteor.Device.isPhone() && (embedMode() || hiddenContextMode()); 134 | }); 135 | 136 | 137 | Handlebars.registerHelper("profileImage", function(user, size) { 138 | var profilePicture = (user && user.profile) ? user.profile.profilePicture : null; 139 | var twitterId = (user && user.services && user.services.twitter) ? user.services.twitter.id : null; 140 | return getProfileImage(profilePicture, twitterId, size); 141 | }); 142 | 143 | 144 | Handlebars.registerHelper("formatNumber", function(num){ 145 | if(!num){ 146 | return 0; 147 | } 148 | return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); 149 | }); 150 | 151 | Handlebars.registerHelper("formatDate", window.formatDate); 152 | Handlebars.registerHelper("formatDateNice", window.formatDateNice); 153 | Handlebars.registerHelper("formatDateCompact", window.formatDateCompact); 154 | 155 | Handlebars.registerHelper("prettyDateInPast", window.prettyDateInPast) 156 | 157 | Handlebars.registerHelper('$eq', 158 | function(v1, v2) { 159 | return (v1 === v2); 160 | } 161 | ); 162 | 163 | Handlebars.registerHelper('capitalize', 164 | function(s) { 165 | return _s.capitalize(s); 166 | } 167 | ); 168 | 169 | Handlebars.registerHelper("hiddenContextMode", function () { 170 | return window.hiddenContextMode(); 171 | }); 172 | 173 | Handlebars.registerHelper("hiddenContextShown", function () { 174 | return window.hiddenContextShown(); 175 | }); 176 | 177 | Handlebars.registerHelper("sandwichMode", function () { 178 | return window.sandwichMode(); 179 | }); 180 | 181 | Handlebars.registerHelper("embedMode", function () { 182 | return window.embedMode(); 183 | }); 184 | 185 | Handlebars.registerHelper("mobileOrTablet", function () { 186 | return window.mobileOrTablet(); 187 | }); 188 | 189 | Handlebars.registerHelper("searchOverlayShown", function () { 190 | return Session.get('searchOverlayShown'); 191 | }); 192 | 193 | 194 | Handlebars.registerHelper("menuOverlayShown", function () { 195 | return Session.get('menuOverlayShown'); 196 | }); 197 | 198 | Handlebars.registerHelper("embedOverlayShown", function () { 199 | return Session.get('embedOverlayShown'); 200 | }); 201 | 202 | Handlebars.registerHelper("howToOverlayShown", function () { 203 | return Session.get('howToOverlayShown'); 204 | }); 205 | 206 | Handlebars.registerHelper("analyticsMode", function () { 207 | return window.analyticsMode(); 208 | }); 209 | 210 | Handlebars.registerHelper("linkActivityShown", function () { 211 | return window.linkActivityShown(); 212 | }); 213 | 214 | Handlebars.registerHelper("cardDataShown", function () { 215 | return window.cardDataShown(); 216 | }); 217 | -------------------------------------------------------------------------------- /client/lib/constants.js: -------------------------------------------------------------------------------- 1 | window.GOOGLE_API_CLIENT_KEY = Meteor.settings["public"].GOOGLE_API_CLIENT_KEY; 2 | 3 | if (!GOOGLE_API_CLIENT_KEY) { 4 | console.error('Settings must be loaded for apis to work'); 5 | throw new Meteor.Error('Settings must be loaded for apis to work'); 6 | } 7 | 8 | window.panelColor = "#815ed9"; 9 | window.remixColor = panelColor; 10 | window.orangeColor = "#fc521f"; 11 | window.actionColor = '#00c976'; 12 | window.dangerColor = '#fc521f'; 13 | window.whiteColor = "white"; 14 | -------------------------------------------------------------------------------- /client/lib/devices.js: -------------------------------------------------------------------------------- 1 | Meteor.Device.emptyUserAgentDeviceName = 'bot'; 2 | Meteor.Device.botUserAgentDeviceName = 'bot'; 3 | Meteor.Device.unknownUserAgentDeviceType = 'bot'; 4 | 5 | // Don't forget to re-detect the device! 6 | Meteor.Device.detectDevice(); 7 | -------------------------------------------------------------------------------- /client/lib/notifications.js: -------------------------------------------------------------------------------- 1 | window.notifyRemix = function(message){ 2 | $.amaran({ 3 | content: { 4 | message: message, 5 | color: 'white', 6 | bgcolor: '#EA1D75' // social-color 7 | }, 8 | 'position' :'top right', 9 | theme:'colorful' 10 | } 11 | ); 12 | }; 13 | 14 | window.notifyFeature = window.notifyRemix; 15 | 16 | window.notifySuccess = function(message){ 17 | $.amaran({ 18 | content: { 19 | message: message, 20 | color: 'white', 21 | bgcolor: '#1DB259' // action-color 22 | }, 23 | 'position' :'top right', 24 | theme:'colorful' 25 | } 26 | ); 27 | }; 28 | 29 | window.notifyLogin = function(){ 30 | var user = Meteor.user(); 31 | var name = user.profile.name ? user.profile.name.split(' ')[0] : user.profile.displayUsername; 32 | notifySuccess('Welcome ' + name + '!'); 33 | }; 34 | 35 | 36 | window.notifyError = function(message){ 37 | $.amaran({ 38 | content: { 39 | message: message, 40 | color: 'white', 41 | bgcolor: '#ff1b0c' // danger-color 42 | }, 43 | 'position' :'top right', 44 | theme:'colorful', 45 | delay: 8000 46 | } 47 | ); 48 | }; 49 | 50 | window.notifyInfo = function(message){ 51 | $.amaran({ 52 | content: { 53 | message: message, 54 | color: 'white', 55 | bgcolor: '#585094' // social-color 56 | }, 57 | 'position' :'top right', 58 | theme:'colorful' 59 | } 60 | ); 61 | }; 62 | 63 | window.notifyBrowser = function(){ 64 | $.amaran({ 65 | content: { 66 | message: "Hi! We're so glad you're writing a story on FOLD. Feel free to try out our editor in any browser and give us feedback, but for the best experience right now, we recommend using Chrome!", 67 | color: 'white', 68 | bgcolor: '#585094' // social-color 69 | }, 70 | sticky: true, 71 | 'position' :'top right', 72 | theme:'colorful' 73 | } 74 | ); 75 | }; 76 | 77 | window.notifyDeploy = function(message, sticky){ 78 | $.amaran({ 79 | content: { 80 | message: message, 81 | color: 'white', 82 | bgcolor: '#585094' // social-color 83 | }, 84 | 'position' :'top right', 85 | theme:'colorful', 86 | sticky: sticky, 87 | clearAll: true 88 | } 89 | ); 90 | $('.amaran').addClass('migration-notification'); 91 | }; 92 | 93 | window.notifyImageSizeError = function(){ 94 | notifyError("Wow, that's a really big file! Can you make it any smaller? We support files up to " + CLOUDINARY_FILE_SIZE/1000000 + ' MB'); 95 | }; 96 | -------------------------------------------------------------------------------- /client/lib/reload.js: -------------------------------------------------------------------------------- 1 | window.readyToMigrate = new ReactiveVar(false); 2 | 3 | var reloadDelay = Meteor.settings['public'].NODE_ENV === 'development' ? 0 : 2000; 4 | 5 | Reload._onMigrate('fold', function (retry) { 6 | if (readyToMigrate.get()) { 7 | return [true, {codeReloaded: true}]; 8 | } else { 9 | //if (Router.current().route.getName() === 'edit') { 10 | if (Meteor.settings['public'].NODE_ENV !== 'development') { 11 | notifyDeploy("We've just made an improvement! Click here to sync up the latest code.", true); 12 | trackEvent('Reload notification happened', {label: 'Reload on click'}); 13 | $('.migration-notification').click(function () { 14 | saveCallback(null, true); 15 | setTimeout(function () { 16 | readyToMigrate.set(true); 17 | retry(); 18 | }, 300); 19 | }); 20 | Router.onRun(function () { 21 | readyToMigrate.set(true); 22 | retry(); 23 | }); 24 | return [false]; 25 | } else { 26 | notifyDeploy("We've made an improvement! Wait just a moment while we sync up the latest code.", false); 27 | trackEvent('Reload notification happened', {label: 'Immediate reload', nonInteraction: 1}); 28 | setTimeout(function () { 29 | readyToMigrate.set(true); 30 | retry(); 31 | }, reloadDelay); 32 | return [false] 33 | } 34 | } 35 | }); 36 | 37 | var migrationData = Reload._migrationData('fold'); 38 | 39 | if (migrationData){ 40 | window.codeReloaded = migrationData.codeReloaded; 41 | } 42 | -------------------------------------------------------------------------------- /client/login.html: -------------------------------------------------------------------------------- 1 | 46 | 47 | 118 | 119 | 120 | 158 | 159 | 160 | 172 | 173 | 174 | 175 | 200 | -------------------------------------------------------------------------------- /client/privacy.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 83 | -------------------------------------------------------------------------------- /client/profile.html: -------------------------------------------------------------------------------- 1 | 34 | 35 | 36 | 80 | 81 | 117 | 118 | 141 | 142 | 156 | 157 | 170 | 171 | 172 | 182 | 183 | 214 | 215 | 226 | -------------------------------------------------------------------------------- /client/profile.js: -------------------------------------------------------------------------------- 1 | var formatDate, weekDays; 2 | 3 | var numStoriesToDisplay = 12; 4 | 5 | weekDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; 6 | 7 | formatDate = function(date) { 8 | var hms; 9 | hms = date.toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, "$1"); 10 | return weekDays[date.getDay()] + " " + (date.getMonth() + 1) + "/" + date.getDate() + "/" + date.getFullYear() + " " + hms; 11 | }; 12 | 13 | Template.profile.onCreated(function(){ 14 | this.sectionToShow = new ReactiveVar('latest'); 15 | this.autorun(() => { 16 | if(adminMode()){ 17 | this.subscribe('adminOtherUserPub', this.data.user._id); 18 | } 19 | }); 20 | }); 21 | 22 | Template.profile.events({ 23 | "click .show-latest" (e, t) { 24 | t.sectionToShow.set('latest'); 25 | }, 26 | "click .show-favorites" (e, t) { 27 | t.sectionToShow.set('favorites'); 28 | }, 29 | "click .show-following" (e, t) { 30 | t.sectionToShow.set('following'); 31 | }, 32 | "click .show-followers" (e, t) { 33 | t.sectionToShow.set('followers'); 34 | }, 35 | "click .followers-total" (e, t) { 36 | t.sectionToShow.set('followers'); 37 | }, 38 | "click .following-total" (e, t) { 39 | t.sectionToShow.set('following'); 40 | } 41 | }); 42 | 43 | Template.profile.helpers({ 44 | "showLatest" (){ 45 | return Template.instance().sectionToShow.get() === 'latest'; 46 | }, 47 | "showFavorites" (){ 48 | return Template.instance().sectionToShow.get() === 'favorites'; 49 | }, 50 | "showFollowing" (){ 51 | return Template.instance().sectionToShow.get() === 'following'; 52 | }, 53 | "showFollowers" (){ 54 | return Template.instance().sectionToShow.get() === 'followers'; 55 | } 56 | }); 57 | 58 | 59 | Template.my_stories.events({ 60 | 'click .unpublish' (){ 61 | if (confirm('Are you sure you want to unpublish this story?')){ 62 | $('.story[data-story-id=' + this._id + ']').fadeOut(500, () => { 63 | Meteor.call('unpublishStory', this._id, (err, result) => { 64 | if(err || !result){ 65 | notifyError('Unpublish failed.'); 66 | } 67 | }); 68 | }) 69 | 70 | } 71 | }, 72 | 'click .delete' (){ 73 | if (confirm('Are you sure you want to delete this story? This cannot be undone.')){ 74 | $('.story[data-story-id=' + this._id + ']').fadeOut(500, () => { 75 | Meteor.call('deleteStory', this._id, (err, result) => { 76 | if(err || !result){ 77 | notifyError('Delete failed.'); 78 | } 79 | }); 80 | }) 81 | 82 | } 83 | } 84 | }); 85 | Template.my_stories.helpers({ 86 | publishedStories () { 87 | if (Meteor.user()) { 88 | return Stories.find({ 89 | authorId: Meteor.userId(), 90 | published : true 91 | }); 92 | } 93 | }, 94 | unpublishedStories () { 95 | if (Meteor.user()) { 96 | return Stories.find({ 97 | authorId: Meteor.userId(), 98 | published : false 99 | }); 100 | } 101 | }, 102 | lastEditDate () { 103 | return prettyDateInPast(this.savedAt); 104 | }, 105 | lastPublishDate () { 106 | return prettyDateInPast(this.publishedAt); 107 | } 108 | }); 109 | 110 | Template.my_stories.events({ 111 | "click div#delete" (d) { 112 | var srcE, storyId; 113 | srcE = d.srcElement ? d.srcElement : d.target; 114 | storyId = $(srcE).closest('div.story').data('story-id'); 115 | return Stories.remove({ 116 | _id: storyId 117 | }); 118 | } 119 | }); 120 | 121 | Template.user_profile.onCreated(function(){ 122 | 123 | this.autorun(() => { // TODO this sometimes runs twice unnecessarily if coming from home (first one does not have full profile user loaded with favorites) 124 | var user = Meteor.users.findOne(this.data.user._id); 125 | var usersFromStories = Stories.find({ published: true, _id: {$in: user.profile.favorites || []}}, {fields: {authorId:1}, reactive: false}).map(function(story){return story.authorId}); 126 | 127 | var usersToSubscribeTo = _.compact(_.union(usersFromStories, user.profile.following, user.followers)); 128 | 129 | this.subscribe('minimalUsersPub', _.sortBy(usersToSubscribeTo, _.identity)); 130 | }); 131 | 132 | this.editing = new ReactiveVar(false); 133 | this.uploadPreview = new ReactiveVar(); 134 | this.uploadingPicture = new ReactiveVar(); 135 | this.pictureId = new ReactiveVar(); 136 | }); 137 | 138 | Template.user_profile.onRendered(function(){ 139 | this.$('.bio').linkify({linkAttributes: {rel : 'nofollow'}}); 140 | }); 141 | 142 | 143 | var ownProfile = function() { 144 | var user = Meteor.user(); 145 | return (user && (user.username == this.user.username)) ? true : false 146 | }; 147 | 148 | Template.user_profile.helpers({ 149 | editing () { 150 | return Template.instance().editing.get() 151 | }, 152 | ownProfile: ownProfile, 153 | name () { 154 | return this.user.profile.name 155 | }, 156 | uploadPreview (){ 157 | return Template.instance().uploadPreview.get(); 158 | }, 159 | uploadingPicture (){ 160 | return Template.instance().uploadingPicture.get(); 161 | }, 162 | "email" (){ 163 | return this.user.emails ? this.user.emails[0].address : null; 164 | }, 165 | bioHtml (){ 166 | return _.escape(this.user.profile.bio).replace(/(@\w+)/g, "$1"); 167 | } 168 | }); 169 | 170 | Template.user_profile.events({ 171 | "click .edit-profile" (d, template) { 172 | template.editing.set(true); 173 | }, 174 | "click .save-profile-button" (d, template) { 175 | template.editing.set(false); 176 | if (template.pictureId.get()) { 177 | Meteor.call('saveProfilePicture', this.user._id, template.pictureId.get()); 178 | } 179 | }, 180 | "change input[type=file]" (e, template){ 181 | var file = _.first(e.target.files); 182 | if (file) { 183 | if(file.size > CLOUDINARY_FILE_SIZE){ 184 | return notifyImageSizeError(); 185 | } 186 | template.uploadingPicture.set(true); 187 | // actual upload 188 | Cloudinary.upload([file], {}, function(err, doc) { 189 | template.uploadingPicture.set(false); 190 | if(err){ 191 | var input = template.$('input[type=file]'); 192 | input.val(null); 193 | input.change(); 194 | notifyError('Image upload failed'); 195 | } else { 196 | template.uploadPreview.set('//res.cloudinary.com/' + Meteor.settings['public'].CLOUDINARY_CLOUD_NAME + '/image/upload/w_150,h_150,c_fill,g_face/' + doc.public_id); 197 | template.pictureId.set(doc.public_id); 198 | } 199 | }) 200 | } else { 201 | template.uploadPreview.set(null); 202 | } 203 | } 204 | }); 205 | 206 | Template.user_stories.onCreated(function(){ 207 | this.seeAllPublished = new ReactiveVar(false); 208 | }); 209 | 210 | Template.user_stories.events({ 211 | "click .toggle-published" (d, template) { 212 | return template.seeAllPublished.set(!template.seeAllPublished.get()) 213 | } 214 | }); 215 | 216 | Template.user_stories.helpers({ 217 | seeAllPublished () { 218 | return Template.instance().seeAllPublished.get() 219 | }, 220 | publishedStories () { 221 | var limit = 0; // = Template.instance().seeAllPublished.get() ? 0 : numStoriesToDisplay; //when limit=0 -> no limit on stories 222 | return Stories.find({authorId : this.user._id, published : true}, { 223 | sort: { 224 | publishedAt: -1 225 | }, 226 | limit: limit 227 | }) 228 | }, 229 | showAllPublishedButton () { 230 | return Stories.find({authorId : this.user._id, published : true}).count() > numStoriesToDisplay 231 | }, 232 | hasPublished () { 233 | return Stories.findOne({authorId : this.user._id, published : true}) 234 | }, 235 | hasDrafts (){ 236 | return Stories.findOne({authorId : this.user._id}, {published: false}) 237 | }, 238 | ownProfile: ownProfile 239 | }); 240 | 241 | Template.user_favorite_stories.onCreated(function(){ 242 | this.seeAllFavorites = new ReactiveVar(false); 243 | }); 244 | 245 | Template.user_favorite_stories.events({ 246 | "click .toggle-favorites" (d, template) { 247 | return template.seeAllFavorites.set(!template.seeAllFavorites.get()) 248 | } 249 | }); 250 | 251 | Template.user_favorite_stories.helpers({ 252 | seeAllFavorites () { 253 | return Template.instance().seeAllFavorites.get() 254 | }, 255 | favoriteStories () { 256 | var limit = 0; // Template.instance().seeAllFavorites.get() ? 0 : numStoriesToDisplay; 257 | var favorites = this.user.profile.favorites; 258 | if (favorites && favorites.length) { 259 | return Stories.find({ 260 | _id: { 261 | $in: this.user.profile.favorites 262 | }}, { 263 | sort: { 264 | publishedAt: -1 265 | }, 266 | limit: limit 267 | }) 268 | } else { 269 | return []; 270 | } 271 | }, 272 | showAllFavoritesButton () { 273 | var favorites = this.user.profile.favorites; 274 | if (favorites && favorites.length) { 275 | return favorites.length > numStoriesToDisplay 276 | } 277 | }, 278 | hasFavorites () { 279 | return !_.isEmpty(this.user.profile.favorites); 280 | }, 281 | ownProfile: ownProfile 282 | }); 283 | 284 | Template.user_following.helpers({ 285 | usersFollowing () { 286 | var following = this.user.profile.following; 287 | if (following && following.length) { 288 | return Meteor.users.find({ 289 | _id: { 290 | $in: following 291 | }}) 292 | } else { 293 | return []; 294 | } 295 | }, 296 | ownProfile: ownProfile 297 | }); 298 | 299 | Template.user_followers.helpers({ 300 | followers () { 301 | var followers = this.user.followers; 302 | if (followers && followers.length) { 303 | return Meteor.users.find({ 304 | _id: { 305 | $in: followers 306 | }}) 307 | } else { 308 | return []; 309 | } 310 | }, 311 | ownProfile: ownProfile 312 | }); 313 | 314 | Template.person_card.helpers({ 315 | profileUrl (){ 316 | return '/profile/' + (Template.instance().data.person.displayUsername); 317 | }, 318 | }); 319 | -------------------------------------------------------------------------------- /client/read.html: -------------------------------------------------------------------------------- 1 | 17 | 18 | 28 | 29 | 49 | 50 | 73 | 74 | 114 | 115 | 145 | -------------------------------------------------------------------------------- /client/recover_password.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /client/recover_password.js: -------------------------------------------------------------------------------- 1 | Template.recover_password_form.onCreated(function() { 2 | this.message = new ReactiveVar(''); 3 | }) 4 | 5 | Template.recover_password_form.helpers({ 6 | message () { 7 | return Template.instance().message.get(); 8 | } 9 | }) 10 | 11 | Template.recover_password_form.events({ 12 | 'submit #recover-password-form' (e, t) { 13 | e.preventDefault(); 14 | 15 | var forgotPasswordForm = $(e.currentTarget); 16 | var email = t.$('#recover-password-email').val().toLowerCase(); 17 | 18 | if(_.isEmpty(email)) { 19 | t.message.set('Please fill in all required fields.'); 20 | return; 21 | } 22 | 23 | if(!SimpleSchema.RegEx.Email.test(email)) { 24 | t.message.set('Please enter a valid email address.'); 25 | return; 26 | } 27 | 28 | if (t.disableSubmit){ 29 | return false 30 | } else { 31 | t.disableSubmit = true; 32 | } 33 | 34 | Accounts.forgotPassword({email: email}, function(err) { 35 | t.disableSubmit = false; 36 | if (err) { 37 | if (err.message === 'User not found [403]') { 38 | t.message.set('This email does not exist.'); 39 | } else { 40 | t.message.set('We are sorry but something went wrong.'); 41 | } 42 | } else { 43 | t.message.set('Email sent, expect it within a few minutes.'); 44 | t.disableSubmit = true; // prevent double submit 45 | } 46 | }); 47 | return false 48 | }, 49 | }); 50 | -------------------------------------------------------------------------------- /client/reset_password.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /client/reset_password.js: -------------------------------------------------------------------------------- 1 | Template.reset_password_form.onCreated(function() { 2 | this.message = new ReactiveVar(''); 3 | }) 4 | 5 | Template.reset_password_form.helpers({ 6 | message () { 7 | return Template.instance().message.get(); 8 | }, 9 | resetPassword (){ 10 | return Session.get('resetPasswordToken'); 11 | } 12 | }) 13 | 14 | Template.reset_password_form.events({ 15 | 'submit #reset-password-form' (e, t) { 16 | e.preventDefault(); 17 | 18 | var password = t.$('#reset-password-password').val(); 19 | var passwordConfirm = t.$('#reset-password-password-confirm').val(); 20 | 21 | if (_.isEmpty(password)) { 22 | t.message.set('Please fill in all required fields.'); 23 | return; 24 | } 25 | 26 | if (!isValidPassword(password)) { 27 | t.message.set('Please enter a valid password.'); 28 | return; 29 | } 30 | 31 | if (password !== passwordConfirm) { 32 | t.message.set('Your two passwords are not equivalent.'); 33 | return; 34 | } 35 | 36 | Accounts.resetPassword(Session.get('resetPasswordToken'), password, function(err) { 37 | if (err) { 38 | t.message.set('We are sorry but something went wrong.'); 39 | } else { 40 | t.message.set('Your password has been successfully changed. Welcome back!'); 41 | Meteor.setTimeout( function(){ 42 | Router.go('home') 43 | }, 1500); 44 | } 45 | }); 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /client/search-results.js: -------------------------------------------------------------------------------- 1 | 2 | SearchResults = new Mongo.Collection(null, { 3 | transform (doc) { return window.newTypeSpecificContextBlock(doc) } 4 | }); 5 | 6 | var options = { 7 | keepHistory: 1000 * 60 * 5, 8 | localSearch: true 9 | }; 10 | var fields = ['title', 'keywords', 'authorName', 'authorDisplayUsername']; 11 | 12 | StorySearch = new SearchSource('stories', fields, options); 13 | PersonSearch = new SearchSource('people', ['profile.name', 'username'], options); 14 | -------------------------------------------------------------------------------- /client/signup.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /client/styles/MyFontsWebfontsKit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * MyFonts Webfont Build ID 3109905, 2015-10-18T16:18:30-0400 4 | * 5 | * The fonts listed in this notice are subject to the End User License 6 | * Agreement(s) entered into by the website owner. All other parties are 7 | * explicitly restricted from using the Licensed Webfonts(s). 8 | * 9 | * You may obtain a valid license at the URLs below. 10 | * 11 | * Webfont: FF Mark Web Italic by FontFont 12 | * URL: http://www.myfonts.com/fonts/fontfont/mark/ot-italic/ 13 | * 14 | * Webfont: FF Mark Web Bold Italic by FontFont 15 | * URL: http://www.myfonts.com/fonts/fontfont/mark/ot-bold-italic/ 16 | * 17 | * Webfont: FF Mark Web Bold by FontFont 18 | * URL: http://www.myfonts.com/fonts/fontfont/mark/ot-bold/ 19 | * 20 | * Webfont: FF Mark Web Light Italic by FontFont 21 | * URL: http://www.myfonts.com/fonts/fontfont/mark/ot-light-italic/ 22 | * 23 | * Webfont: FF Mark Web Light by FontFont 24 | * URL: http://www.myfonts.com/fonts/fontfont/mark/ot-light/ 25 | * 26 | * Webfont: FF Mark Web Medium Italic by FontFont 27 | * URL: http://www.myfonts.com/fonts/fontfont/mark/ot-medium-italic/ 28 | * 29 | * Webfont: FF Mark Web Medium by FontFont 30 | * URL: http://www.myfonts.com/fonts/fontfont/mark/ot-medium/ 31 | * 32 | * Webfont: FF Mark Web by FontFont 33 | * URL: http://www.myfonts.com/fonts/fontfont/mark/ot-regular/ 34 | * 35 | * 36 | * License: http://www.myfonts.com/viewlicense?type=web&buildid=3109905 37 | * Licensed pageviews: 50,000 38 | * Webfonts copyright: 2013 published by FontShop International GmbH 39 | * 40 | * © 2015 MyFonts Inc 41 | */ 42 | 43 | 44 | /* 45 | * @license 46 | * MyFonts Webfont Build ID 3109936, 2015-10-18T18:52:23-0400 47 | * 48 | * The fonts listed in this notice are subject to the End User License 49 | * Agreement(s) entered into by the website owner. All other parties are 50 | * explicitly restricted from using the Licensed Webfonts(s). 51 | * 52 | * You may obtain a valid license at the URLs below. 53 | * 54 | * Webfont: FF Magda Clean Mono Web Pro Regular by FontFont 55 | * URL: http://www.myfonts.com/fonts/fontfont/ff-magda-clean-mono/pro-regular/ 56 | * Copyright: 2011 Critzla, Cornel Windlin, Henning Krause published by FSI FontShop International GmbH 57 | * Licensed pageviews: 50,000 58 | * 59 | * 60 | * License: http://www.myfonts.com/viewlicense?type=web&buildid=3109936 61 | * 62 | * © 2015 MyFonts Inc 63 | */ 64 | 65 | 66 | @font-face {font-family: 'FFMagdaCleanMonoWebProRegular';src: url('webfonts/2F7430_0_0.eot');src: url('webfonts/2F7430_0_0.eot?#iefix') format('embedded-opentype'),url('webfonts/2F7430_0_0.woff2') format('woff2'),url('webfonts/2F7430_0_0.woff') format('woff'),url('webfonts/2F7430_0_0.ttf') format('truetype');} 67 | 68 | 69 | @font-face {font-family: 'FFMarkWebItalic';src: url('webfonts/2F7411_0_0.eot');src: url('webfonts/2F7411_0_0.eot?#iefix') format('embedded-opentype'),url('webfonts/2F7411_0_0.woff2') format('woff2'),url('webfonts/2F7411_0_0.woff') format('woff'),url('webfonts/2F7411_0_0.ttf') format('truetype');} 70 | 71 | 72 | @font-face {font-family: 'FFMarkWebBoldItalic';src: url('webfonts/2F7411_1_0.eot');src: url('webfonts/2F7411_1_0.eot?#iefix') format('embedded-opentype'),url('webfonts/2F7411_1_0.woff2') format('woff2'),url('webfonts/2F7411_1_0.woff') format('woff'),url('webfonts/2F7411_1_0.ttf') format('truetype');} 73 | 74 | 75 | @font-face {font-family: 'FFMarkWebBold';src: url('webfonts/2F7411_2_0.eot');src: url('webfonts/2F7411_2_0.eot?#iefix') format('embedded-opentype'),url('webfonts/2F7411_2_0.woff2') format('woff2'),url('webfonts/2F7411_2_0.woff') format('woff'),url('webfonts/2F7411_2_0.ttf') format('truetype');} 76 | 77 | 78 | @font-face {font-family: 'FFMarkWebLightItalic';src: url('webfonts/2F7411_3_0.eot');src: url('webfonts/2F7411_3_0.eot?#iefix') format('embedded-opentype'),url('webfonts/2F7411_3_0.woff2') format('woff2'),url('webfonts/2F7411_3_0.woff') format('woff'),url('webfonts/2F7411_3_0.ttf') format('truetype');} 79 | 80 | 81 | @font-face {font-family: 'FFMarkWebLight';src: url('webfonts/2F7411_4_0.eot');src: url('webfonts/2F7411_4_0.eot?#iefix') format('embedded-opentype'),url('webfonts/2F7411_4_0.woff2') format('woff2'),url('webfonts/2F7411_4_0.woff') format('woff'),url('webfonts/2F7411_4_0.ttf') format('truetype');} 82 | 83 | 84 | @font-face {font-family: 'FFMarkWebMediumItalic';src: url('webfonts/2F7411_5_0.eot');src: url('webfonts/2F7411_5_0.eot?#iefix') format('embedded-opentype'),url('webfonts/2F7411_5_0.woff2') format('woff2'),url('webfonts/2F7411_5_0.woff') format('woff'),url('webfonts/2F7411_5_0.ttf') format('truetype');} 85 | 86 | 87 | @font-face {font-family: 'FFMarkWebMedium';src: url('webfonts/2F7411_6_0.eot');src: url('webfonts/2F7411_6_0.eot?#iefix') format('embedded-opentype'),url('webfonts/2F7411_6_0.woff2') format('woff2'),url('webfonts/2F7411_6_0.woff') format('woff'),url('webfonts/2F7411_6_0.ttf') format('truetype');} 88 | 89 | 90 | @font-face {font-family: 'FFMarkWeb';src: url('webfonts/2F7411_7_0.eot');src: url('webfonts/2F7411_7_0.eot?#iefix') format('embedded-opentype'),url('webfonts/2F7411_7_0.woff2') format('woff2'),url('webfonts/2F7411_7_0.woff') format('woff'),url('webfonts/2F7411_7_0.ttf') format('truetype');} 91 | 92 | /* FOLD Edit: Set bold weight of markweb to the bold version of the font*/ 93 | @font-face {font-family: 'FFMarkWeb';src: url('webfonts/2F7411_2_0.eot');src: url('webfonts/2F7411_2_0.eot?#iefix') format('embedded-opentype'),url('webfonts/2F7411_2_0.woff2') format('woff2'),url('webfonts/2F7411_2_0.woff') format('woff'),url('webfonts/2F7411_2_0.ttf') format('truetype'); 94 | font-weight: bold;} 95 | -------------------------------------------------------------------------------- /client/styles/about.lessimport: -------------------------------------------------------------------------------- 1 | a.green { 2 | &:hover { 3 | color: @action-color; 4 | } 5 | } 6 | 7 | a.underline{ 8 | text-decoration: underline; 9 | } 10 | 11 | .contact-footer { 12 | position: relative; 13 | bottom:0; 14 | height: 50px; 15 | width: 100%; 16 | margin-top: 45px; 17 | text-align: center; 18 | font-size: 12px; 19 | padding: 15px; 20 | background-color: white; 21 | z-index: 5; 22 | a { 23 | color: @action-color; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /client/styles/amaran.min.css: -------------------------------------------------------------------------------- 1 | .amaran-overlay{position:fixed;width:100%;height:100%;top:0;left:0;background:rgba(153,204,51,.9);display:block;z-index:777}.amaran-overlay .amaran-wrapper{z-index:9999} 2 | .amaran.awesome{width:300px;min-height:65px;background:#f3f3f3;color:#222;margin:15px;padding:5px 5px 5px 70px;font-family:"Open Sans",Helvetica,Arial,sans-serif;font-size:16px;font-weight:600;box-shadow:1px 1px 1px #000}.amaran.awesome .icon{width:50px;height:50px;position:absolute;top:50%;left:10px;background:#000;margin-top:-25px;border-radius:50%;text-align:center;line-height:50px;font-size:22px}.amaran.awesome p{padding:0;margin:0}.amaran.awesome p span{font-weight:300}.amaran.awesome p span.light{font-size:13px;display:block;color:#777}.amaran.awesome.ok p.bold{color:#178B13}.amaran.awesome.ok .icon{background-color:#178B13;color:#fff}.amaran.awesome.error p.bold{color:#D82222}.amaran.awesome.error .icon{background-color:#D82222;color:#fff}.amaran.awesome.warning p.bold{color:#9F6000}.amaran.awesome.warning .icon{background-color:#9F6000;color:#fff}.amaran.awesome.yellow p.bold{color:#CFA846}.amaran.awesome.yellow .icon{background-color:#CFA846;color:#fff}.amaran.awesome.blue p.bold{color:#2980b9}.amaran.awesome.blue .icon{background-color:#2980b9;color:#fff}.amaran.awesome.green p.bold{color:#27ae60}.amaran.awesome.green .icon{background-color:#27ae60;color:#fff}.amaran.awesome.purple p.bold{color:#5B54AA}.amaran.awesome.purple .icon{background-color:#5B54AA;color:#fff} 3 | .amaran.colorful{width:300px;min-height:45px;overflow:hidden;background-color:transparent;z-index:1}.amaran.colorful .colorful-inner{width:100%;min-height:45px;display:block;position:relative;background-color:#484860;padding:15px 25px 15px 15px;color:#fff;font-size:14px;border-bottom:1px solid rgba(0,0,0,.2);border-radius:4px}.amaran.colorful .amaran-close{color:#fff;z-index:2;top:8px;right:8px;text-align:center;line-height:18px}.amaran-wrapper.center .amaran.colorful{margin:0 auto} 4 | .amaran.default{width:300px;min-height:45px;background:#1B1E24;background:-webkit-linear-gradient(left,#111213,#111213 15%,#1b1e24 15%,#1b1e24);background:linear-gradient(left,#111213,#111213 15%,#1b1e24 15%,#1b1e24);color:#fff;font-family:"Open Sans",Helvetica,Arial,sans-serif;font-size:13px;font-weight:300;margin:5px;overflow:hidden;border-bottom:1px solid #111213;border-radius:6px}.amaran.default .default-spinner{width:45px;min-height:45px;display:block;float:left;position:relative}.amaran.default .default-spinner span{width:18px;height:18px;background:#27ae60;display:block;border-radius:50%;position:absolute;top:50%;left:50%;margin-left:-11px;margin-top:-9px}.amaran.default .default-message{float:left}.amaran.default .default-message span{padding:3px;line-height:43px}.amaran.default .default-message:after{clear:both} 5 | @charset "UTF-8";.amaran-close,.amaran-sticky{height:20px;top:2px;cursor:pointer}.amaran-wrapper *{box-sizing:border-box}.amaran-wrapper{position:fixed;z-index:9999}.amaran-wrapper.top{top:0;bottom:auto}.amaran-wrapper.bottom{bottom:0;top:auto}.amaran-wrapper.left{left:0}.amaran-wrapper.right{right:0;left:auto}.amaran-wrapper.center{width:50%;height:50%;margin:auto;position:fixed;top:0;left:0;bottom:0;right:0}.amaran{width:200px;background:rgba(0,0,0,.7);padding:3px;color:#fff;border-radius:4px;display:none;font-size:13px;cursor:pointer;position:relative;text-align:left;min-height:50px;margin:10px}.amaran-close,.amaran-sticky{width:20px;display:block;position:absolute}.amaran-close{right:2px}.amaran-close:before{content:"x";color:#fff;font-weight:700;font-family:Arial,sans-serif;font-size:18px}.amaran-sticky{right:20px}.amaran-sticky:before{content:"●";color:#fff;font-weight:700;font-family:Arial,sans-serif;font-size:18px}.amaran-sticky.sticky:before{color:#27ae60} 6 | .amaran.tumblr{width:300px;min-height:45px;overflow:hidden;background-color:#fff;color:#444;border-radius:3px;box-shadow:0 1px 4px rgba(0,0,0,.3);z-index:1}.amaran.tumblr .title{position:relative;font-size:15px;line-height:15px;height:28px;padding:5px 10px;border-bottom:1px solid rgba(0,0,0,.1);font-weight:700;z-index:1}.amaran.tumblr .content{padding:5px}.amaran.tumblr .image{float:left}.amaran.tumblr .amaran-close{z-index:2}.amaran.tumblr .amaran-close:before{color:#000} 7 | .amaran.user{width:300px;min-height:100px;background:#f3f3f3;color:#222;margin:15px;font-family:"Open Sans",Helvetica,Arial,sans-serif;font-size:13px;font-weight:300;box-shadow:1px 1px 1px #000;border-radius:0;padding:0}.amaran.user .icon{width:100px;height:100px;position:relative;background:#000;float:left}.amaran.user img{max-width:100%}.amaran.user .info{padding-left:110px;padding-top:10px}.amaran.user b{display:block;font-size:16px}.amaran.user.blue{background:#2773ed;color:#fff}.amaran.user.yellow{background:#f4b300;color:#fff}.amaran.user.green{background:#78ba00;color:#fff} 8 | -------------------------------------------------------------------------------- /client/styles/icons.lessimport: -------------------------------------------------------------------------------- 1 | .standard-icon-colors; 2 | 3 | .active{ 4 | .bg{ 5 | fill: @action-color; 6 | } 7 | } 8 | 9 | .back-arrow{ 10 | .fg{ 11 | fill: @white-color; 12 | } 13 | } 14 | 15 | svg.icon { 16 | .size-to-fit; 17 | } 18 | svg.add-card-icon{ 19 | height:20px; 20 | width:20px; 21 | } 22 | 23 | svg.browse-arrow { 24 | .size(20px); 25 | .fg{ 26 | fill: @white-color; 27 | } 28 | } 29 | svg.search-icon { 30 | height: 15px; 31 | width: 15px; 32 | margin-top: 2px; 33 | } 34 | 35 | svg.babyburger-icon{ 36 | .size(20px); 37 | &:hover { 38 | .bg{ 39 | fill: @action-color; 40 | } 41 | } 42 | } 43 | 44 | svg.mobile-back-icon{ 45 | .bg{ 46 | fill: @action-color; 47 | } 48 | } 49 | 50 | .star-button{ 51 | @height: 20px; 52 | display: inline-block; 53 | font-size: 18px; 54 | height: @height; 55 | line-height: @height; 56 | button { 57 | height: @height; 58 | width: @height; 59 | svg{ 60 | width: auto; 61 | } 62 | padding: 0; 63 | background-color: transparent; 64 | border: none; 65 | } 66 | } 67 | 68 | .share-button{ 69 | .star-button; 70 | } 71 | 72 | .favorite-button{ 73 | .star-button; 74 | .favorite { 75 | .fg { 76 | fill: @medium-color; 77 | &:hover { 78 | fill: @favorite-color; 79 | } 80 | } 81 | } 82 | .unfavorite{ 83 | .fg{ 84 | fill: @favorite-color; 85 | } 86 | } 87 | .just-favorited{ 88 | .fg { 89 | fill: @favorite-color; 90 | } 91 | svg{ 92 | .animation(grow 0.5s 1); 93 | } 94 | } 95 | .just-unfavorited{ 96 | .fg { 97 | &:hover { 98 | fill: @medium-color; 99 | } 100 | } 101 | } 102 | @keyframes grow{ 103 | 50%{ 104 | .transform(scale(1.25)); 105 | } 106 | } 107 | 108 | 109 | } 110 | 111 | .editors-pick-button{ 112 | .star-button; 113 | .pick{ 114 | .fg{ 115 | fill: @light-color; 116 | &:hover{ 117 | fill: saturate(@orange-color, 80%);; 118 | } 119 | } 120 | } 121 | .unpick{ 122 | .fg{ 123 | fill: @orange-color; 124 | &:hover{ 125 | fill: fade(@orange-color, 80%); 126 | } 127 | } 128 | } 129 | } 130 | 131 | i.loading-icon{ 132 | color: @action-color; 133 | } 134 | 135 | .facebook-social-icon{ 136 | .fg{ 137 | fill: @inactive-color; 138 | } 139 | &:hover{ 140 | .fg{ 141 | fill: @facebook-color; 142 | } 143 | } 144 | } 145 | 146 | .instagram-social-icon{ 147 | .fg{ 148 | fill: @inactive-color; 149 | } 150 | &:hover{ 151 | .fg{ 152 | fill: @instagram-color; 153 | } 154 | } 155 | } 156 | .twitter-social-icon{ 157 | .fg{ 158 | fill: @inactive-color; 159 | } 160 | &:hover{ 161 | .fg{ 162 | fill: @twitter-color; 163 | } 164 | } 165 | } 166 | 167 | .clear-search-icon{ 168 | .fg{ 169 | fill: @inactive-color; 170 | } 171 | .bg{ 172 | fill: @background-color; 173 | } 174 | } 175 | 176 | .follow-flag{ 177 | .fg{ 178 | fill: @white-color; 179 | } 180 | } 181 | 182 | .follow-flag-check{ 183 | .bg{ 184 | fill: @social-color; 185 | } 186 | } 187 | 188 | .follow-flag-plus{ 189 | .bg{ 190 | fill: @inactive-color; 191 | } 192 | } 193 | 194 | .follow-flag-x{ 195 | .bg{ 196 | fill: @orange-color; 197 | } 198 | } 199 | 200 | .social-option-triangle{ 201 | .fg{ 202 | fill: @dark-color 203 | } 204 | } 205 | 206 | .embed-icon{ 207 | .fg{ 208 | fill: @dark-color 209 | } 210 | &:hover{ 211 | .fg{ 212 | fill: @action-color; 213 | } 214 | } 215 | } 216 | 217 | .fold-title{ 218 | height: 100%; 219 | .fg{ 220 | fill: @action-color 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /client/styles/login.lessimport: -------------------------------------------------------------------------------- 1 | @button-height: 45px; 2 | 3 | div.login, div.reset-password, div.recover-password { 4 | width: 100%; 5 | padding-top: 240px; 6 | height: auto; 7 | margin-bottom: 45px; 8 | @media @mobile { 9 | padding-top: 200px; 10 | } 11 | } 12 | 13 | input.error, input.required { 14 | //&:after{ 15 | // content:'no'; 16 | //} 17 | } 18 | input.success{ 19 | //&:after{ 20 | // content:'no'; 21 | //} 22 | } 23 | 24 | @media @not-mobile{ 25 | div.login-left { 26 | position: relative; 27 | float: left; 28 | width: 45%; 29 | } 30 | 31 | div.login-right { 32 | position: relative; 33 | float: left; 34 | width: 45%; 35 | } 36 | } 37 | 38 | a.green { 39 | color: @action-color; 40 | } 41 | 42 | .save-profile-button, .reset-button { 43 | margin-top: 20px; 44 | width: 100px; 45 | background-color: @action-color; 46 | color: @white-color; 47 | height: @button-height; 48 | .FFMarkWebBold; 49 | 50 | &:hover { 51 | background-color: @dark-color; 52 | } 53 | } 54 | 55 | 56 | div.log-in-error { 57 | margin-top: 10px; 58 | } 59 | 60 | @vertical-center: 90px; 61 | 62 | div.divider { 63 | @font-size: 20px; 64 | position: relative; 65 | float: left; 66 | width: 10%; 67 | font-size: @font-size; 68 | text-align: center; 69 | margin-top: @vertical-center - @font-size/2; 70 | 71 | @media @mobile { 72 | width: 100%; 73 | margin-top: @font-size; 74 | margin-bottom: @font-size; 75 | visibility: hidden; 76 | } 77 | } 78 | 79 | div.sign-up-options { 80 | margin: auto; 81 | width: 350px; 82 | a{ 83 | .FFMarkWebBold; 84 | background-color: @action-color; 85 | height: @button-height; 86 | display: block; 87 | color: @white-color; 88 | text-align: center; 89 | line-height: @button-height; 90 | &:hover { 91 | background-color: @dark-color; 92 | } 93 | } 94 | } 95 | 96 | 97 | .signin-overlay{ 98 | z-index: 99999; 99 | position: fixed; 100 | top: 0; 101 | left: 0; 102 | background-color: fade(@dark-color, 80%); 103 | .FFMarkWebMedium; 104 | 105 | .signin-modal{ 106 | 107 | @max-width: calc(100% ~"-" @magic-styling-distance); 108 | @height: 60px; 109 | @width: 380px; 110 | 111 | margin:auto; 112 | display:block; 113 | text-align: center; 114 | background-color: @white-color; 115 | font-size: 18px; 116 | @media @mobile { 117 | font-size: 16px; 118 | } 119 | 120 | .size(500px); 121 | 122 | 123 | button{ 124 | font-size: 18px; 125 | 126 | background-color: @action-color; 127 | color: @white-color; 128 | .loading-icon{ 129 | color: @white-color; 130 | } 131 | .FFMarkWebBold; 132 | &:hover{ 133 | &:not(.no-hover-state){ 134 | background-color: @black-color; 135 | } 136 | } 137 | &:disabled{ 138 | background-color: @inactive-color; 139 | cursor: auto; 140 | } 141 | } 142 | a, button.text{ 143 | background: none !important; 144 | color: @social-color; 145 | padding: 0; 146 | &:hover{ 147 | text-decoration: underline; 148 | } 149 | } 150 | 151 | max-height: 100%; 152 | max-width: 100%; 153 | 154 | .center-over-parent-div; 155 | 156 | .title{ 157 | margin-top: 93px; 158 | &.has-explanation{ 159 | margin-top: 68px; 160 | } 161 | &.absolute{ 162 | position: absolute; 163 | width: 100%; 164 | top: @magic-styling-distance + 5px; 165 | margin-top:0; 166 | text-align: center; 167 | } 168 | img{ 169 | width: 120px; 170 | } 171 | line-height: 0; 172 | } 173 | 174 | .slogan, .explanation{ 175 | margin-top: @magic-styling-distance; 176 | @media @mobile { 177 | margin-top: @magic-styling-distance / 2; 178 | } 179 | line-height: normal; 180 | } 181 | .slogan{ 182 | color: @medium-color; 183 | } 184 | 185 | .explanation{ 186 | color: @dark-color; 187 | line-height: 28px; 188 | white-space: pre-wrap; 189 | } 190 | 191 | .user-menu{ 192 | width: 100%; 193 | margin-top: @magic-styling-distance; 194 | @media @mobile { 195 | margin-top: @magic-styling-distance / 2; 196 | } 197 | text-align: center; 198 | } 199 | 200 | button.signin{ 201 | display: inline-block; 202 | margin-bottom: @magic-styling-distance / 2; 203 | width: @width; 204 | height: @height; 205 | max-width: @max-width; 206 | } 207 | 208 | @input-width: 335px; 209 | @status-width: 60px; 210 | 211 | input{ 212 | height: @height; 213 | width: @input-width; 214 | max-width: calc(100% ~"-" (@status-width + @magic-styling-distance)); 215 | &::-webkit-input-placeholder { 216 | .FFMarkWebBold; 217 | } 218 | &:-moz-placeholder { 219 | .FFMarkWebBold; 220 | } 221 | &:-ms-input-placeholder { 222 | .FFMarkWebBold; 223 | } 224 | margin-top: 30px; 225 | margin-bottom: 0px; 226 | &:first-child{ 227 | margin-top: 0; 228 | } 229 | } 230 | 231 | 232 | .status{ 233 | width: @status-width; 234 | height: 20px; 235 | text-align: center; 236 | display: inline-block; 237 | position: relative; 238 | left: -20px; 239 | svg{ 240 | position: absolute; 241 | } 242 | } 243 | .field-info{ 244 | height: 0; 245 | span{ 246 | width: @input-width + @status-width; 247 | max-width: calc(100% ~"-" (@magic-styling-distance)); 248 | display: inline-block; 249 | text-align: left; 250 | } 251 | text-align: center; 252 | font-size: 12px; 253 | color: @placeholder-color; 254 | } 255 | div.error{ 256 | color: @orange-color; 257 | } 258 | 259 | .back{ 260 | position: absolute; 261 | left: @magic-styling-distance; 262 | top: @magic-styling-distance + 13px; 263 | @media @mobile{ 264 | left: @magic-styling-distance / 2; 265 | top: @magic-styling-distance / 2 + 13px; 266 | } 267 | color: @medium-color !important; 268 | } 269 | 270 | .already-have{ 271 | margin-top: @magic-styling-distance / 2; 272 | max-width: 100%; 273 | } 274 | .close{ 275 | .FFMarkWeb; 276 | position: absolute; 277 | top: @magic-styling-distance; 278 | right: @magic-styling-distance; 279 | @media @mobile{ 280 | top: @magic-styling-distance / 2; 281 | right: @magic-styling-distance / 2; 282 | } 283 | background-color: @medium-color; 284 | .size(@magic-styling-distance); 285 | font-size: @magic-styling-distance/2; 286 | padding: 0; 287 | text-align: center; 288 | } 289 | 290 | form{ 291 | margin-top: 105px; 292 | } 293 | 294 | .accept{ 295 | font-size: 12px; 296 | margin: 0; 297 | margin-top: 15px; 298 | &.above{ 299 | margin-top: 35px; 300 | } 301 | .padding-sides(@magic-styling-distance/2); 302 | color: @placeholder-color; 303 | } 304 | 305 | hr{ 306 | width: @width; 307 | border: 0; 308 | border-top: 1px solid @medium-color; 309 | margin-top: @magic-styling-distance / 2; 310 | margin-bottom: @magic-styling-distance / 2; 311 | } 312 | 313 | #login-form{ 314 | line-height: 0; 315 | input, .lost-or-login{ 316 | width: @width; 317 | max-width: @max-width; 318 | } 319 | .lost-or-login{ 320 | display: inline-block; 321 | line-height: normal; 322 | position: relative; 323 | .forgot-password{ 324 | margin-top: 17px; 325 | float: left; 326 | cursor: pointer; 327 | &:hover{ 328 | text-decoration: underline !important; 329 | } 330 | } 331 | .login-button{ 332 | float: right; 333 | } 334 | .error{ 335 | position: absolute; 336 | font-size: 14px; 337 | line-height: 18px; 338 | bottom: 0; 339 | white-space: pre-line; 340 | 341 | 342 | @media @mobile{ 343 | font-size: 14px; 344 | line-height: 15px; 345 | bottom: 0px; 346 | white-space: normal; 347 | } 348 | text-align: left; 349 | width: calc(100% ~"-" 130px); 350 | } 351 | } 352 | } 353 | 354 | .twitter-signin{ 355 | svg{ 356 | .size(auto); 357 | margin-left: 13px; 358 | display: inline; 359 | vertical-align: middle; 360 | } 361 | span{ 362 | display: inline; 363 | vertical-align: middle; 364 | } 365 | } 366 | 367 | 368 | .signup-button, .login-button{ 369 | display: inline-block; 370 | height: @height; 371 | width: 120px; 372 | text-align: center; 373 | } 374 | .signup-button{ 375 | margin-top: @magic-styling-distance; 376 | } 377 | .login-button{ 378 | margin-top: @magic-styling-distance / 2; 379 | } 380 | 381 | //onboarding 382 | .welcome{ 383 | font-size: 36px; 384 | margin-top: 120px; 385 | margin-bottom: 35px; 386 | } 387 | .author-image{ 388 | height: 95px; 389 | img{ 390 | height: 100%; 391 | } 392 | } 393 | .edit-prompt{ 394 | margin-top: 35px; 395 | margin-bottom: 40px; 396 | } 397 | .finish{ 398 | height: @height; 399 | width: 120px; 400 | } 401 | 402 | //recover-password 403 | #recover-password-form{ 404 | margin-top: 120px; 405 | } 406 | #recover-password-email{ 407 | width: @width; 408 | max-width: @max-width; 409 | } 410 | .recover-button{ 411 | height: @height; 412 | margin-top: @magic-styling-distance/2; 413 | width: @width; 414 | max-width: @max-width; 415 | } 416 | .message{ 417 | margin-top: @magic-styling-distance/2; 418 | } 419 | } 420 | } 421 | -------------------------------------------------------------------------------- /client/styles/metaview.lessimport: -------------------------------------------------------------------------------- 1 | @metaview-padding: 50px; 2 | 3 | .metaview { 4 | position: fixed; 5 | overflow: scroll; 6 | padding-top: @metaview-padding; 7 | padding-left: @metaview-padding; 8 | background-color: rgba(0,0,0,0.9); 9 | z-index: 100; 10 | min-height: 100%; 11 | min-width: 100%; 12 | 13 | button.close { 14 | position: absolute; 15 | top: 20px; 16 | right: 20px; 17 | i { 18 | font-size: 50px; 19 | } 20 | } 21 | 22 | 23 | .cards { 24 | 25 | margin: 0 auto; 26 | overflow: scroll; 27 | display: inline-block; 28 | 29 | .vertical-block, .horizontal-block { 30 | cursor: move; 31 | background-color: @medium-color; 32 | border: 2px solid transparent; 33 | 34 | &:hover { 35 | background-color: @action-color; 36 | border-color: @action-color; 37 | } 38 | } 39 | 40 | h4{ 41 | margin-top: 4px; 42 | margin-bottom: 4px; 43 | } 44 | 45 | p{ 46 | margin-top: 4px; 47 | } 48 | 49 | .row { 50 | display: table; 51 | height: 100px; 52 | margin-bottom: 5px; 53 | 54 | .vertical-block { 55 | padding: 5px 3px; 56 | font-size: 10px; 57 | height: 100%; 58 | width: 160px; 59 | float: left; 60 | white-space: normal; 61 | font-size: 12px; 62 | line-height: 14px; 63 | overflow: auto; 64 | } 65 | 66 | .horizontal-section { 67 | display: inline-block; 68 | min-width: 100px; 69 | height: 90px; 70 | .horizontal-block { 71 | margin-top: 5px; 72 | margin-left: 5px; 73 | height: 80px; 74 | width: 120px; 75 | display: inline-block; 76 | white-space: normal; 77 | line-height: 14px; 78 | font-size: 12px; 79 | overflow: auto; 80 | position: relative; 81 | 82 | &.has-image{ 83 | .vertically-center-images; 84 | text-align: center; 85 | background-color: @dark-color; 86 | } 87 | 88 | background-position: center center; 89 | background-repeat: no-repeat; 90 | -webkit-background-size: cover; 91 | -moz-background-size: cover; 92 | -o-background-size: cover; 93 | background-size: cover; 94 | 95 | img { 96 | width: auto; 97 | max-width: 100%; 98 | margin: auto; 99 | vertical-align: middle; 100 | display: inline-block; 101 | max-height: 100%; 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /client/styles/profile.lessimport: -------------------------------------------------------------------------------- 1 | @picture-radius: 126px; 2 | @profile-width: 340px; 3 | 4 | 5 | h1 { 6 | font-size: 30px; 7 | margin-bottom: 45px; 8 | @media (max-width: 800px) { 9 | text-align: center; 10 | } 11 | } 12 | 13 | .message { 14 | ul{ 15 | margin-top: 10px; 16 | } 17 | li{ 18 | list-style: disc; 19 | margin-left: 20px; 20 | color: @social-color; 21 | } 22 | &.stories-message{ 23 | a{ 24 | &:hover{ 25 | text-decoration: underline !important; 26 | } 27 | } 28 | } 29 | button{ 30 | color: @white-color; 31 | height: @magic-styling-distance; 32 | .FFMarkWebBold; 33 | font-size: 13px; 34 | .padding-sides(20px); 35 | } 36 | button.create-story, button.view-drafts, .random-story, .random-person{ 37 | margin-top: @magic-styling-distance/2; 38 | } 39 | .create-story{ 40 | background-color: @action-color; 41 | } 42 | .view-drafts{ 43 | background-color: @social-color; 44 | } 45 | 46 | } 47 | 48 | div.profile-section { 49 | padding-top: 120px; 50 | height: 100%; 51 | position: fixed; 52 | 53 | @media (max-width: 800px) { 54 | position: inherit; 55 | } 56 | @media @mobile { 57 | padding-top: @magic-styling-distance * 2; 58 | } 59 | } 60 | 61 | 62 | div.user-profile { 63 | position: fixed; 64 | padding: @magic-styling-distance/2; 65 | background-color: @white-color; 66 | width: @profile-width; 67 | max-width: 340px; 68 | min-height: 700px; 69 | height: 100%; 70 | z-index: 5; 71 | border-right: 1px solid @inactive-color; 72 | 73 | @media (max-width: 800px) { 74 | @margin-side: 5%; 75 | margin-left: @margin-side; 76 | max-width: 100%; 77 | width: calc(100% ~"-" @margin-side*2); 78 | height: auto; 79 | min-height: 0; 80 | position: initial; 81 | margin-top: 30px; 82 | border-right: none; 83 | } 84 | 85 | @media (max-height: 640px) { 86 | position: initial; 87 | 88 | } 89 | 90 | div.picture { 91 | background-color: @light-transparent-color; 92 | width: @picture-radius; 93 | height: @picture-radius; 94 | border-radius: @picture-radius; 95 | margin: @magic-styling-distance/2 calc(50% ~"-" @picture-radius*0.5) 20px; 96 | overflow: hidden; 97 | position: relative; 98 | margin-bottom: 35px; 99 | 100 | .profile-picture-large { 101 | height: @picture-radius; 102 | width: @picture-radius; 103 | } 104 | } 105 | 106 | .following-followers{ 107 | margin-top: @magic-styling-distance; 108 | .padding-sides(35px); 109 | .display-flex; 110 | text-align: center; 111 | color: @social-color; 112 | font-size: 13px; 113 | line-height: 17px; 114 | .FFMarkWebBold; 115 | div{ 116 | display: inline-block; 117 | .flex(1); 118 | cursor: pointer; 119 | } 120 | } 121 | 122 | .follow-button-container{ 123 | text-align: center; 124 | margin-top: @magic-styling-distance - 2px; 125 | } 126 | .follow-button{ 127 | display: inline-block; 128 | } 129 | 130 | div.name, div.bio { 131 | text-align: center; 132 | word-wrap:break-word; 133 | margin: 15px 30px; 134 | } 135 | div.name { 136 | font-size: 20px; 137 | .FFMarkWebBold; 138 | margin-bottom: 10px; 139 | line-height: 120%; 140 | } 141 | div.bio { 142 | .FFMarkWeb; 143 | line-height: 160%; 144 | a{ 145 | color: @action-color; 146 | } 147 | } 148 | .edit-profile { 149 | color: @social-color; 150 | position: absolute; 151 | .FFMarkWebBold; 152 | font-size: 13px; 153 | &:hover{ 154 | text-decoration: underline; 155 | } 156 | } 157 | .save-profile-button { 158 | position: absolute; 159 | top: 0px; 160 | @media (max-width: 800px), (max-height: 640px) { 161 | top: 150px; 162 | } 163 | } 164 | .bio-form { 165 | .FFMarkWeb; 166 | margin-top: 5px; 167 | margin-bottom: 15px; 168 | height: 110px; 169 | border: 1px solid @medium-color; 170 | padding: 10px 18px; 171 | font-size: 16px; 172 | line-height: 22px; 173 | } 174 | } 175 | 176 | .toggle-published, .toggle-favorites { 177 | .FFMarkWebBold; 178 | font-size: 13px; 179 | padding: 0; 180 | color: @dark-color; 181 | display: block; 182 | text-align: center; 183 | 184 | @media (max-width: 800px) { 185 | top: -14px; 186 | left: 0px; 187 | margin: 0 auto; 188 | width: 100%; 189 | } 190 | &:hover { 191 | text-decoration: underline; 192 | color: @action-color; 193 | } 194 | 195 | } 196 | 197 | .my-stories-buttons{ 198 | position: absolute; 199 | width: 100%; 200 | bottom: 12px; 201 | padding-top: 70px; 202 | button{ 203 | color: white; 204 | padding: 10px; 205 | } 206 | .button-group{ 207 | float:right; 208 | } 209 | .unpublish{ 210 | margin-right: 10px; 211 | background-color: @social-color; 212 | &:hover{ 213 | background-color: darken(@social-color, 10%); 214 | } 215 | } 216 | .delete{ 217 | margin-right: 15px; 218 | background-color: @less-danger-color; 219 | &:hover{ 220 | background-color: darken(@less-danger-color, 10%); 221 | } 222 | } 223 | } 224 | 225 | .profile{ 226 | .message{ 227 | @media @mobile { 228 | .padding-sides(20px); 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /client/styles/reset_password.lessimport: -------------------------------------------------------------------------------- 1 | .reset-password, .recover-password { 2 | div.form-container { 3 | width: 350px; 4 | margin: 0 auto; 5 | 6 | h1 { 7 | text-align: center; 8 | margin-bottom: 50px; 9 | font-size: 23px; 10 | } 11 | 12 | form { 13 | width: 350px; 14 | font-size: 15px; 15 | .FFMarkWeb; 16 | margin: auto; 17 | } 18 | 19 | .message { 20 | margin-top: 10px; 21 | } 22 | 23 | .reset-button { 24 | width: 150px; 25 | } 26 | 27 | .recover-button { 28 | margin-top: 20px; 29 | width: 250px; 30 | background-color: @action-color; 31 | color: @white-color; 32 | height: @button-height; 33 | 34 | &:hover { 35 | background-color: @dark-color; 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /client/styles/story.lessimport: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/client/styles/story.lessimport -------------------------------------------------------------------------------- /client/styles/styles.less: -------------------------------------------------------------------------------- 1 | // Top-level universal elements (e.g. header and footer), mixins, utility functions 2 | // For now, includes re-used elements 3 | @import "icons.lessimport"; 4 | @import "layout.lessimport"; 5 | 6 | // Re-used elements in all story views 7 | @import "story.lessimport"; 8 | 9 | // Page-specific 10 | @import "create.lessimport"; 11 | @import "home.lessimport"; 12 | @import "about.lessimport"; 13 | @import "login.lessimport"; 14 | @import "profile.lessimport"; 15 | @import "reset_password.lessimport"; 16 | 17 | // Thing-specific 18 | @import "metaview.lessimport"; 19 | @import "widgets.lessimport"; 20 | -------------------------------------------------------------------------------- /client/styles/widgets.lessimport: -------------------------------------------------------------------------------- 1 | 2 | .sod_select, .sod_select * { 3 | -webkit-box-sizing: border-box; 4 | -moz-box-sizing: border-box; 5 | box-sizing: border-box; 6 | -webkit-touch-callout: none; 7 | -webkit-user-select: none; 8 | -moz-user-select: none; 9 | -ms-user-select: none; 10 | user-select: none; 11 | } 12 | 13 | /* The SoD - Please keep this first three lines intact, otherwise all hell will break looooooose */ 14 | .sod_select { 15 | display: inline-block; 16 | position: relative; 17 | line-height: 1; 18 | 19 | width: 180px; 20 | padding: 14px 10px; 21 | border: 1px solid @medium-color; 22 | background: #ffffff; 23 | color: #444444; 24 | font-size: 14px; 25 | .text-align-start; 26 | outline: 0; 27 | outline-offset: -2px; /* Opera */ 28 | cursor: default; 29 | 30 | // Up/down arrows 31 | @arrow-size: 5px; 32 | &:before, &:after { 33 | content: ""; 34 | border-left: @arrow-size solid transparent; 35 | border-right: @arrow-size solid transparent; 36 | border-top: @arrow-size solid @dark-color; 37 | position: absolute; 38 | right: 15px; 39 | top: 18px; 40 | } 41 | 42 | &.open { 43 | &:before, &:after { 44 | border-bottom: @arrow-size solid @action-color; 45 | border-top: none; 46 | } 47 | } 48 | 49 | 50 | // // Down arrow 51 | // &:after { 52 | // content: "▾"; 53 | // top: auto; 54 | // bottom: 12px; 55 | // } 56 | 57 | // Change the border color on hover, focus and when open 58 | &:hover, &.open, &.focus { 59 | border-color: #000000; 60 | } 61 | &.open { color: #919191; } 62 | &.focus { box-shadow: 0 0 5px rgba(0,0,0,.2); } 63 | 64 | // When the entire SoD is disabled, go crazy! 65 | &.disabled { 66 | border-color: #828282; 67 | color: #b2b2b2; 68 | cursor: not-allowed; 69 | } 70 | 71 | // The "label", or whatever we should call it. Keep the first three lines for truncating. 72 | .sod_label { 73 | display: block; 74 | overflow: hidden; 75 | white-space: nowrap; 76 | text-overflow: ellipsis; 77 | 78 | padding-right: 15px; 79 | } 80 | 81 | // Options list wrapper 82 | .sod_list_wrapper { 83 | position: absolute; 84 | top: 100%; 85 | left: 0; 86 | display: none; 87 | height: auto; 88 | width: 180px; 89 | margin: 0 0 0 -1px; 90 | background: #ffffff; 91 | border: 1px solid black; 92 | border-top: none; 93 | color: #444444; 94 | z-index: 1; 95 | } 96 | 97 | /* Shows the option list (don't edit) */ 98 | &.open .sod_list_wrapper { display: block; } 99 | 100 | /* Don't display the options when */ 101 | &.disabled.open .sod_list_wrapper { display: none; } 102 | 103 | /* When the option list is displayed above the SoD */ 104 | &.above .sod_list_wrapper { 105 | top: auto; 106 | bottom: 100%; 107 | border-top: 3px solid #000000; 108 | border-bottom: none; 109 | } 110 | 111 | // Options list container 112 | .sod_list { 113 | display: block; 114 | overflow-y: auto; 115 | padding: 0; 116 | margin: 0; 117 | } 118 | 119 | .sod_option { 120 | display: block; 121 | overflow: hidden; 122 | white-space: nowrap; 123 | text-overflow: ellipsis; 124 | position: relative; 125 | padding: 14px 10px; 126 | list-style-type: none; 127 | 128 | &.optgroup, &.optgroup.disabled { 129 | background: inherit; 130 | color: #939393; 131 | font-size: 10px; 132 | font-style: italic; 133 | } 134 | 135 | &.groupchild { padding-left: 20px; } 136 | 137 | &.is-placeholder { display: none; } 138 | 139 | &.disabled { 140 | background: inherit; 141 | color: #cccccc 142 | } 143 | 144 | &.active { 145 | background: #f7f7f7; 146 | color: #333333; 147 | 148 | } 149 | 150 | &.selected { 151 | font-weight: 500; 152 | padding-right: 25px; 153 | } 154 | 155 | &.selected:before { 156 | content: ""; 157 | position: absolute; 158 | right: 10px; 159 | top: 50%; 160 | .transform(translateY(-50%)); 161 | display: inline-block; 162 | color: #808080; 163 | height: 9px; 164 | width: 10px; 165 | background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNy4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgMTAgOSIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTAgOSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBmaWxsPSIjRDlEOUQ4IiBkPSJNNCw2LjdDMy42LDYuMywzLjUsNi4xLDMuMSw1LjdDMi42LDUuMiwyLDQuNiwxLjUsNC4xYy0wLjgtMC44LTIsMC40LTEuMiwxLjJjMC45LDAuOSwxLjksMS45LDIuOCwyLjgNCgkJYzAuNywwLjcsMS4zLDEsMiwwQzYuNyw2LDguMywzLjcsOS44LDEuNUMxMC41LDAuNSw5LTAuMyw4LjMsMC42bDAsMEM2LjcsMi45LDUuNyw0LjQsNCw2LjciLz4NCjwvZz4NCjwvc3ZnPg0K); 166 | } 167 | } 168 | 169 | 170 | // Hide native select 171 | select { display: none !important; } 172 | 173 | // The native select in touch mode. Keep this first line. Sorry, keep everything. 174 | &.touch select { 175 | -webkit-appearance: menulist-button; 176 | 177 | position: absolute; 178 | top: 0; 179 | left: 0; 180 | display: block !important; 181 | height: 100%; 182 | width: 100%; 183 | opacity: 0; 184 | z-index: 1; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /client/unsubscribe.html: -------------------------------------------------------------------------------- 1 | 18 | 19 | -------------------------------------------------------------------------------- /client/unsubscribe.js: -------------------------------------------------------------------------------- 1 | Template.unsubscribe.onCreated(function(){ 2 | this.unsubscribed = new ReactiveVar(false); 3 | this.resubscribed = new ReactiveVar(false); 4 | 5 | this.autorun(() => { 6 | if(Meteor.userId()){ 7 | Meteor.call('unsubscribe', Router.current().params.query.email_type, (err, success) => { 8 | if(err || !success){ 9 | notifyError('Unsubscribe failed. Please email us at info@readfold.com') 10 | } else { 11 | this.unsubscribed.set(true); 12 | } 13 | }) 14 | } else { 15 | openSignInOverlay('Please sign in to unsubscribe from emails'); 16 | } 17 | }) 18 | 19 | }); 20 | 21 | Template.unsubscribe.events({ 22 | 'click .resubscribe' (e, t){ 23 | Meteor.call('resubscribe', Router.current().params.query.email_type, (err, success) => { 24 | if (err) { 25 | notifyError('Resubscribe failed. Please email us at info@readfold.com') 26 | } else { 27 | t.resubscribed.set(true); 28 | } 29 | }) 30 | } 31 | }); 32 | 33 | Template.unsubscribe.helpers({ 34 | 'unsubscribed' (){ 35 | return Template.instance().unsubscribed.get(); 36 | }, 37 | 'resubscribed' (){ 38 | return Template.instance().resubscribed.get(); 39 | }, 40 | 'humanReadableEmailType' (){ 41 | 42 | switch (Router.current().params.query.email_type){ 43 | case 'followed-you': 44 | return 'Follower Notifications'; 45 | break; 46 | case 'following-published': 47 | return 'Notifications When Someone You Follow Publishes a Story'; 48 | break; 49 | default: 50 | return Router.current().params.query.email_type; 51 | } 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /collections/user-collections.js: -------------------------------------------------------------------------------- 1 | if(!this.Schema){ 2 | Schema = {}; 3 | } 4 | 5 | Schema.UserProfile = new SimpleSchema({ 6 | name: { 7 | type: String, 8 | optional: true, 9 | min: 2, 10 | max: 127, 11 | autoValue () { // trim off whitespace 12 | if (this.isSet && typeof this.value === "string") { 13 | return this.value.trim(); 14 | } else { 15 | this.unset() 16 | } 17 | } 18 | }, 19 | bio: { 20 | type: String, 21 | optional: true, 22 | max: 160, 23 | autoValue () { // trim off whitespace 24 | if (this.isSet && typeof this.value === "string") { 25 | return this.value.trim(); 26 | } else { 27 | this.unset() 28 | } 29 | }, 30 | autoform: { 31 | rows: 7 32 | } 33 | }, 34 | favorites: { 35 | type: [String], 36 | optional: true, 37 | defaultValue: [] 38 | }, 39 | following: { 40 | type: [String], 41 | optional: true, 42 | defaultValue: [] 43 | }, 44 | profilePicture: { 45 | type: String, 46 | autoValue () { 47 | var monster = _.random(1,10).toString(); 48 | if (this.isSet) { 49 | return this.value; 50 | } else if (this.isInsert) { 51 | return this.value || monster; 52 | } else if (this.isUpsert) { 53 | return {$setOnInsert: this.value || monster}; 54 | } else { 55 | this.unset(); 56 | } 57 | } 58 | } 59 | }); 60 | 61 | Schema.User = new SimpleSchema({ 62 | username: { 63 | type: String, 64 | regEx: /^[a-z0-9_]*$/, 65 | min: 3, 66 | max: 15, 67 | optional: true, 68 | autoValue () { 69 | if (this.isSet && typeof this.value === "string") { 70 | return this.value.toLowerCase().trim(); 71 | } else { 72 | this.unset() 73 | } 74 | } 75 | }, 76 | displayUsername: { // allows for caps 77 | type: String, 78 | optional: true, 79 | autoValue () { // TODO ensure this matches username except for capitalization 80 | if (this.isSet && typeof this.value === "string") { 81 | return this.value.trim(); 82 | } else { 83 | this.unset() 84 | } 85 | } 86 | }, 87 | tempUsername: { 88 | type: String, 89 | optional: true 90 | }, 91 | emails: { 92 | type: [Object], 93 | optional: true 94 | }, 95 | "emails.$.address": { 96 | type: String, 97 | regEx: SimpleSchema.RegEx.Email, 98 | label: "Email address", 99 | autoValue () { 100 | if (this.isSet && typeof this.value === "string") { 101 | return this.value.toLowerCase(); 102 | } else { 103 | this.unset(); 104 | } 105 | }, 106 | autoform: { 107 | afFieldInput: { 108 | readOnly: true, 109 | disabled: true 110 | } 111 | } 112 | }, 113 | "emails.$.verified": { 114 | type: Boolean 115 | }, 116 | createdAt: { 117 | type: Date, 118 | autoValue () { 119 | if (this.isInsert) { 120 | return new Date; 121 | } else if (this.isUpsert) { 122 | return {$setOnInsert: new Date}; 123 | } else { 124 | this.unset(); 125 | } 126 | } 127 | }, 128 | admin: { 129 | type: Boolean, 130 | optional: true, 131 | autoValue (){ 132 | this.unset(); // don't allow to be set from anywhere within the code 133 | } 134 | }, 135 | accessPriority: { 136 | type: Number, 137 | optional: true 138 | }, 139 | profile: { 140 | type: Schema.UserProfile, 141 | optional: true, 142 | defaultValue: {} 143 | }, 144 | followers: { 145 | type: [String], 146 | optional: true, 147 | defaultValue: [] 148 | }, 149 | followersTotal: { 150 | type: Number, 151 | optional: true, 152 | defaultValue: 0 153 | }, 154 | followingTotal: { 155 | type: Number, 156 | optional: true, 157 | defaultValue: 0 158 | }, 159 | services: { 160 | type: Object, 161 | optional: true, 162 | blackbox: true 163 | }, 164 | unsubscribes: { 165 | type: [String], 166 | allowedValues: ['followed-you', 'following-published'], 167 | optional: true 168 | } 169 | }); 170 | 171 | 172 | Meteor.users.attachSchema(Schema.User); 173 | 174 | SimpleSchema.messages({ 175 | "regEx username": "Username may only contain letters, numbers, and underscores" 176 | }); 177 | -------------------------------------------------------------------------------- /collections/user-methods.js: -------------------------------------------------------------------------------- 1 | Meteor.methods({ 2 | saveProfilePicture (userId, pictureId) { 3 | check(userId, String); 4 | check(pictureId, String); 5 | if (this.userId === userId) { 6 | Meteor.users.update({ 7 | _id: this.userId 8 | }, { 9 | $set: { 10 | "profile.profilePicture": pictureId 11 | } 12 | }); 13 | } else { 14 | throw new Meteor.Error("Only the account owner may edit this profile") 15 | } 16 | }, 17 | updateProfile (modifier, userId) { // TO-DO cleanup 18 | check(userId, String); 19 | check(modifier, Object); 20 | 21 | var bio, name, newName; 22 | var modifierSet = modifier.$set; 23 | var modifierUnset = modifier.$unset; 24 | 25 | var setObject = {}; 26 | if (bio = modifierSet['profile.bio']) { 27 | check(bio, String); 28 | setObject['profile.bio'] = bio; 29 | } else if (modifierUnset['profile.bio'] === "") { 30 | setObject['profile.bio'] = ''; 31 | } 32 | 33 | if (name = modifierSet['profile.name']) { 34 | check(name, String); 35 | setObject['profile.name'] = name; 36 | } else if (modifierUnset['profile.name'] === "") { 37 | setObject['profile.name'] = ''; 38 | } 39 | 40 | if (this.userId === userId) { 41 | if (newName = setObject['profile.name']) { 42 | if (newName !== Meteor.user().profile.name) { 43 | Stories.update({authorId: this.userId}, { // update authorName on stories if name changed 44 | $set: { 45 | authorName: newName 46 | } 47 | }) 48 | } 49 | } 50 | 51 | Meteor.users.update({ 52 | _id: this.userId 53 | }, { 54 | $set: setObject 55 | }); 56 | } else { 57 | throw new Meteor.Error("Only the account owner may edit this profile") 58 | } 59 | } 60 | }); 61 | 62 | 63 | -------------------------------------------------------------------------------- /lib/activities.js: -------------------------------------------------------------------------------- 1 | infoFor = function(type, id){ 2 | switch(type){ 3 | case 'Person': 4 | var user = Meteor.users.findOne(id, {fields: {'profile.name' : 1, 'profile.profilePicture' : 1, 'services.twitter.id' : 1, 'displayUsername': 1}}); 5 | var userInfo = { 6 | id: user._id, 7 | type: 'Person', 8 | name: user.profile.name, 9 | urlPath: '/profile/' + user.displayUsername, 10 | imageId: user.profile.profilePicture 11 | }; 12 | 13 | if (user.services && user.services.twitter){ 14 | _.extend(userInfo, { 15 | twitterId: user.services.twitter.id 16 | }); 17 | } 18 | 19 | return userInfo; 20 | case 'Story': 21 | var story = Stories.findOne({_id: id, published: true}, {fields: {'title' : 1, 'userPathSegment': 1, 'storyPathSegment': 1, 'headerImage': 1, authorId: 1, authorDisplayUsername: 1, authorUsername: 1 }}); 22 | return { 23 | id: story._id, 24 | type: 'Story', 25 | name: story.title, 26 | urlPath: '/read/' + story.userPathSegment + '/' + story.storyPathSegment, 27 | imageId: story.headerImage, 28 | attributedTo: { 29 | id: story.authorId, 30 | type: 'Person', 31 | name: story.authorDisplayUsername || story.authorUsername, 32 | urlPath: '/profile/' + story.authorDisplayUsername || story.authorUsername 33 | } 34 | }; 35 | default: 36 | throw new Meteor.Error('Type not found for infoFor') 37 | } 38 | }; 39 | 40 | generateFavoriteActivity = function(userId, storyId){ 41 | if(Meteor.isServer){ 42 | Meteor.defer(function(){ // make non-blocking 43 | check(userId, String); 44 | check(storyId, String); 45 | 46 | generateActivity('Favorite', { 47 | actor: infoFor('Person', userId), 48 | object: infoFor('Story', storyId) 49 | }) 50 | }) 51 | } 52 | }; 53 | 54 | generateFollowActivity = function(userId, userToFollowId){ 55 | if(Meteor.isServer){ 56 | 57 | Meteor.defer(function(){ // make non-blocking 58 | check(userId, String); 59 | check(userToFollowId, String); 60 | 61 | var userToFollow = Meteor.users.findOne(userToFollowId, {fields: {'profile.following': 1}}); 62 | var activityType = _.contains(userToFollow.profile.following, userId) ? 'FollowBack' : 'Follow'; 63 | 64 | generateActivity(activityType, { 65 | actor: infoFor('Person', userId), 66 | object: infoFor('Person', userToFollowId) 67 | }) 68 | }) 69 | } 70 | }; 71 | 72 | generatePublishActivity = function(userId, storyId){ 73 | if(Meteor.isServer){ 74 | Meteor.defer(function(){ // make non-blocking 75 | check(userId, String); 76 | check(storyId, String); 77 | 78 | generateActivity('Publish', { 79 | actor: infoFor('Person', userId), 80 | object: infoFor('Story', storyId) 81 | }) 82 | }) 83 | } 84 | }; 85 | 86 | generateShareActivity = function(storyId, service){ 87 | if(Meteor.isServer){ 88 | Meteor.defer(function(){ // make non-blocking 89 | check(storyId, String); 90 | check(service, String); 91 | 92 | generateActivity('Share', { 93 | content: service, 94 | object: infoFor('Story', storyId) 95 | }); 96 | }) 97 | } 98 | }; 99 | 100 | generateViewThresholdActivity = function(storyId, viewCount){ 101 | if(Meteor.isServer){ 102 | Meteor.defer(function(){ // make non-blocking 103 | check(storyId, String); 104 | check(viewCount, Number); 105 | 106 | generateActivity('ViewThreshold', { 107 | content: viewCount, 108 | object: infoFor('Story', storyId) 109 | }); 110 | }) 111 | } 112 | }; 113 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | PUB_SIZE = 30; 2 | if (Meteor.isClient){ 3 | window.PUB_SIZE = PUB_SIZE; 4 | } 5 | 6 | CLOUDINARY_FILE_SIZE = 20000000; // bytes 7 | 8 | VIEW_THRESHOLDS = [ 9 | 25, 10 | 50, 11 | 75, 12 | 100, 13 | 150, 14 | 200, 15 | 250, 16 | 300, 17 | 350, 18 | 400, 19 | 450, 20 | 500, 21 | 600, 22 | 700, 23 | 800, 24 | 900, 25 | 1000, 26 | 1250, 27 | 1500, 28 | 1750, 29 | 2000, 30 | 2250, 31 | 2500, 32 | 2750, 33 | 3000, 34 | 3250, 35 | 3500, 36 | 3750, 37 | 4000, 38 | 4250, 39 | 4500, 40 | 4750, 41 | 5000, 42 | 5500, 43 | 6000, 44 | 6500, 45 | 7000, 46 | 7500, 47 | 8000, 48 | 8500, 49 | 9000, 50 | 9500, 51 | 10000, 52 | 11000, 53 | 12000, 54 | 13000, 55 | 14000, 56 | 15000, 57 | 20000, 58 | 30000, 59 | 40000, 60 | 50000, 61 | 60000, 62 | 70000, 63 | 80000, 64 | 90000, 65 | 100000, 66 | 200000, 67 | 500000, 68 | 1000000 69 | ]; 70 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | newTypeSpecificContextBlock = function (doc) { 2 | switch (doc.type) { 3 | case 'stream': 4 | return new Stream(doc); 5 | case 'video': 6 | return new VideoBlock(doc); 7 | case 'text': 8 | return new TextBlock(doc); 9 | case 'map': 10 | return new MapBlock(doc); 11 | case 'image': 12 | return new ImageBlock(doc); 13 | case 'gif': 14 | return new GifBlock(doc); 15 | case 'audio': 16 | return new AudioBlock(doc); 17 | case 'viz': 18 | return new VizBlock(doc); 19 | case 'twitter': 20 | return new TwitterBlock(doc); 21 | case 'link': 22 | return new LinkBlock(doc); 23 | case 'news': 24 | return new NewsBlock(doc); 25 | case 'action': 26 | return new ActionBlock(doc); 27 | default: 28 | return new ContextBlock(doc); 29 | } 30 | }; 31 | 32 | idFromPathSegment = function(pathSegment) { // everything after last dash 33 | return pathSegment.substring(pathSegment.lastIndexOf('-') + 1); 34 | }; 35 | 36 | sum = function(a,b){ return a+b; }; 37 | 38 | if(Meteor.isServer){ 39 | import cheerio from 'cheerio'; 40 | } 41 | 42 | getProfileImage = function(profilePicture, twitterId, size, forEmail){ 43 | var diameter; 44 | if (size === 'large'){ 45 | diameter = 150; 46 | } else { 47 | diameter = 60; 48 | } 49 | var defaultProfilePic = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; // transparent gif 50 | var dprSetting = ((typeof window == 'undefined') || window.isHighDensity) ? ',dpr_2.0' : ''; 51 | var twitterPic; 52 | if (twitterId) { 53 | twitterPic = '//res.cloudinary.com/' + Meteor.settings['public'].CLOUDINARY_CLOUD_NAME + '/image/twitter/w_' + diameter + ',h_' + diameter + ',c_fill,g_face' + dprSetting + '/' + twitterId 54 | } 55 | 56 | 57 | if (profilePicture || twitterId) { 58 | if ( profilePicture) { 59 | if ( profilePicture < 20) { // it's a monster 60 | if (twitterPic){ 61 | return twitterPic 62 | } else { // show monster 63 | if(forEmail){ 64 | return '//res.cloudinary.com/' + Meteor.settings['public'].CLOUDINARY_CLOUD_NAME + '/w_' + diameter + ',h_' + diameter + dprSetting + '/static/profile_monster_' + profilePicture + '.png'; 65 | } else { 66 | return '//res.cloudinary.com/' + Meteor.settings['public'].CLOUDINARY_CLOUD_NAME + '/static/profile_monster_' + profilePicture + '.svg'; 67 | } 68 | } 69 | } else { 70 | return '//res.cloudinary.com/' + Meteor.settings['public'].CLOUDINARY_CLOUD_NAME + '/image/upload/w_' + diameter + ',h_' + diameter + ',c_fill,g_face' + dprSetting + '/' + profilePicture 71 | } 72 | } else if (twitterPic) { 73 | return twitterPic 74 | } 75 | } 76 | 77 | // if nothing else served up 78 | return defaultProfilePic 79 | } 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fold", 3 | "version": "1.0.0", 4 | "description": "FOLD is a platform allowing storytellers to structure and contextualize stories", 5 | "main": "./start", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/readFOLD/FOLD.git" 12 | }, 13 | "author": "Joe Goldbeck & Alexis Hope", 14 | "bugs": { 15 | "url": "https://github.com/readFOLD/FOLD/issues" 16 | }, 17 | "homepage": "https://readfold.com", 18 | "dependencies": { 19 | "babel-runtime": "^6.20.0", 20 | "bcrypt": "^1.0.1", 21 | "cheerio": "0.19.0", 22 | "meteor-node-stubs": "^0.2.4", 23 | "prerender-node": "2.0.2", 24 | "twit": "1.1.20", 25 | "vimeo-api": "1.1.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /public/2014_Ebola_virus_epidemic_in_West_Africa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/2014_Ebola_virus_epidemic_in_West_Africa.png -------------------------------------------------------------------------------- /public/Deceased_per_day_Ebola_2014.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/Deceased_per_day_Ebola_2014.png -------------------------------------------------------------------------------- /public/EbolaCycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/EbolaCycle.png -------------------------------------------------------------------------------- /public/Ebola_Betten_Isolation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/Ebola_Betten_Isolation.jpg -------------------------------------------------------------------------------- /public/Ebola_Virus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/Ebola_Virus.jpg -------------------------------------------------------------------------------- /public/alright_sans.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/alright_sans.woff -------------------------------------------------------------------------------- /public/alright_sans_bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/alright_sans_bold.woff -------------------------------------------------------------------------------- /public/batsmonkeys.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/batsmonkeys.jpg -------------------------------------------------------------------------------- /public/cdc_doctor_discards.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/cdc_doctor_discards.jpg -------------------------------------------------------------------------------- /public/ebola_isolation_chamber.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/ebola_isolation_chamber.jpg -------------------------------------------------------------------------------- /public/embedtest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Runsudshdushud!! 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/js/responsive-embed.js: -------------------------------------------------------------------------------- 1 | var minWidthBeforeShrink = 435; 2 | var minHeightBeforeShrink = 435; 3 | var shrunkenWidth = 300; 4 | var shrunkenHeight = 200; 5 | 6 | var isMobile = false; //initiate as false 7 | // device detection 8 | if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent) 9 | || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0,4))) isMobile = true; 10 | 11 | var forEach = function (array, callback, scope) { 12 | for (var i = 0; i < array.length; i++) { 13 | callback.call(scope, i, array[i]); 14 | } 15 | }; 16 | 17 | var iterateOverFoldIFrames = function(cb){ 18 | forEach(document.querySelectorAll('iframe[src*="//fold.cm"]'), cb) 19 | }; 20 | 21 | // from underscorejs.org 22 | var throttle = function(func, wait, options) { 23 | var context, args, result; 24 | var timeout = null; 25 | var previous = 0; 26 | if (!options) options = {}; 27 | var later = function() { 28 | previous = options.leading === false ? 0 : Date.now(); 29 | timeout = null; 30 | result = func.apply(context, args); 31 | if (!timeout) context = args = null; 32 | }; 33 | return function() { 34 | var now = Date.now(); 35 | if (!previous && options.leading === false) previous = now; 36 | var remaining = wait - (now - previous); 37 | context = this; 38 | args = arguments; 39 | if (remaining <= 0 || remaining > wait) { 40 | if (timeout) { 41 | clearTimeout(timeout); 42 | timeout = null; 43 | } 44 | previous = now; 45 | result = func.apply(context, args); 46 | if (!timeout) context = args = null; 47 | } else if (!timeout && options.trailing !== false) { 48 | timeout = setTimeout(later, remaining); 49 | } 50 | return result; 51 | }; 52 | }; 53 | 54 | var resizeIFrames = function(){ 55 | iterateOverFoldIFrames(function(i, iframe){ 56 | iframe.style.maxHeight = ''; 57 | iframe.style.maxWidth = ''; 58 | if(iframe.offsetWidth < minWidthBeforeShrink || iframe.offsetHeight < minHeightBeforeShrink || isMobile){ 59 | iframe.style.maxHeight = shrunkenHeight + "px"; 60 | iframe.style.maxWidth = shrunkenWidth + "px"; 61 | } 62 | }) 63 | }; 64 | 65 | var throttledResize = throttle(resizeIFrames, 100); 66 | 67 | window.addEventListener("load", function(event) { 68 | resizeIFrames(); 69 | if(!isMobile){ 70 | window.addEventListener('resize', throttledResize, true); 71 | } 72 | }); 73 | -------------------------------------------------------------------------------- /public/nurses_1976.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/nurses_1976.jpg -------------------------------------------------------------------------------- /public/webfonts/2F7411_0_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_0_0.eot -------------------------------------------------------------------------------- /public/webfonts/2F7411_0_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_0_0.ttf -------------------------------------------------------------------------------- /public/webfonts/2F7411_0_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_0_0.woff -------------------------------------------------------------------------------- /public/webfonts/2F7411_0_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_0_0.woff2 -------------------------------------------------------------------------------- /public/webfonts/2F7411_1_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_1_0.eot -------------------------------------------------------------------------------- /public/webfonts/2F7411_1_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_1_0.ttf -------------------------------------------------------------------------------- /public/webfonts/2F7411_1_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_1_0.woff -------------------------------------------------------------------------------- /public/webfonts/2F7411_1_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_1_0.woff2 -------------------------------------------------------------------------------- /public/webfonts/2F7411_2_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_2_0.eot -------------------------------------------------------------------------------- /public/webfonts/2F7411_2_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_2_0.ttf -------------------------------------------------------------------------------- /public/webfonts/2F7411_2_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_2_0.woff -------------------------------------------------------------------------------- /public/webfonts/2F7411_2_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_2_0.woff2 -------------------------------------------------------------------------------- /public/webfonts/2F7411_3_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_3_0.eot -------------------------------------------------------------------------------- /public/webfonts/2F7411_3_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_3_0.ttf -------------------------------------------------------------------------------- /public/webfonts/2F7411_3_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_3_0.woff -------------------------------------------------------------------------------- /public/webfonts/2F7411_3_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_3_0.woff2 -------------------------------------------------------------------------------- /public/webfonts/2F7411_4_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_4_0.eot -------------------------------------------------------------------------------- /public/webfonts/2F7411_4_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_4_0.ttf -------------------------------------------------------------------------------- /public/webfonts/2F7411_4_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_4_0.woff -------------------------------------------------------------------------------- /public/webfonts/2F7411_4_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_4_0.woff2 -------------------------------------------------------------------------------- /public/webfonts/2F7411_5_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_5_0.eot -------------------------------------------------------------------------------- /public/webfonts/2F7411_5_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_5_0.ttf -------------------------------------------------------------------------------- /public/webfonts/2F7411_5_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_5_0.woff -------------------------------------------------------------------------------- /public/webfonts/2F7411_5_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_5_0.woff2 -------------------------------------------------------------------------------- /public/webfonts/2F7411_6_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_6_0.eot -------------------------------------------------------------------------------- /public/webfonts/2F7411_6_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_6_0.ttf -------------------------------------------------------------------------------- /public/webfonts/2F7411_6_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_6_0.woff -------------------------------------------------------------------------------- /public/webfonts/2F7411_6_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_6_0.woff2 -------------------------------------------------------------------------------- /public/webfonts/2F7411_7_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_7_0.eot -------------------------------------------------------------------------------- /public/webfonts/2F7411_7_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_7_0.ttf -------------------------------------------------------------------------------- /public/webfonts/2F7411_7_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_7_0.woff -------------------------------------------------------------------------------- /public/webfonts/2F7411_7_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_7_0.woff2 -------------------------------------------------------------------------------- /public/webfonts/2F7430_0_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7430_0_0.eot -------------------------------------------------------------------------------- /public/webfonts/2F7430_0_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7430_0_0.ttf -------------------------------------------------------------------------------- /public/webfonts/2F7430_0_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7430_0_0.woff -------------------------------------------------------------------------------- /public/webfonts/2F7430_0_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7430_0_0.woff2 -------------------------------------------------------------------------------- /reset: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | meteor reset 3 | exit 4 | -------------------------------------------------------------------------------- /server/accounts.js: -------------------------------------------------------------------------------- 1 | validateNewUser = function(user){ 2 | if (user.username){ // only if an email user. if twitter user will do this later 3 | if (user.emails && user.emails[0]){ 4 | return checkUserSignup(user.username, user.emails[0].address); 5 | } else { 6 | throw new Meteor.Error('Please enter your email') 7 | } 8 | } else { 9 | return true 10 | } 11 | }; 12 | 13 | Accounts.validateNewUser(validateNewUser); 14 | 15 | if (!Meteor.settings.NEW_USER_ACCESS_PRIORITY) { 16 | throw new Meteor.Error('Meteor.settings.NEW_USER_ACCESS_PRIORITY is required') 17 | } 18 | 19 | Accounts.onCreateUser(function(options, user) { 20 | if(!options || !user) { 21 | throw new Meteor.Error('Error creating user'); 22 | return; 23 | } 24 | 25 | if (options.profile) { 26 | user.profile = options.profile; 27 | } else { 28 | user.profile = {}; 29 | } 30 | 31 | if (user.username === 'author') { 32 | user.accessPriority = options.accessPriority; 33 | } else { 34 | user.accessPriority = parseInt(Meteor.settings.NEW_USER_ACCESS_PRIORITY); 35 | } 36 | 37 | if (user.services.twitter) { // twitter signup 38 | user.tempUsername = user.services.twitter.screenName; 39 | } else { // email signup 40 | user.displayUsername = options.username; 41 | Meteor.defer(function(){ 42 | sendWelcomeEmail(user); 43 | }); 44 | } 45 | 46 | return user; 47 | }); 48 | 49 | // Password Reset E-mail 50 | Accounts.emailTemplates.from = 'FOLD Accounts '; 51 | Accounts.emailTemplates.siteName = 'readfold.com', 52 | 53 | Accounts.emailTemplates.resetPassword.subject = function(user, url) { 54 | return 'FOLD Password Reset'; 55 | }; 56 | 57 | Accounts.emailTemplates.resetPassword.text = function(user, url) { 58 | url = url.replace('#/', '') 59 | return "To reset your password, simply click the link below:\n\n" + url + "\n\n" + "Happy FOLDing!\nFOLD Team\nhttps://readfold.com"; 60 | }; 61 | -------------------------------------------------------------------------------- /server/activities.js: -------------------------------------------------------------------------------- 1 | generateActivity = function(type, details){ 2 | check(type, String); 3 | check(details, Object); 4 | 5 | if(details.fanout){ 6 | throw new Meteor.Error('Fanout should not be set'); 7 | } 8 | var fullDetails = _.extend({}, details, {type: type}); 9 | 10 | var dedupDetails = { 11 | type: type 12 | }; 13 | 14 | _.each(['actor', 'object', 'target'], function(key){ 15 | if(details[key]){ 16 | dedupDetails[key +'.id'] = details[key].id; 17 | } 18 | }); 19 | 20 | switch(type){ 21 | case 'Share': 22 | // pass through 23 | break; 24 | case 'Message': 25 | // pass through 26 | break; 27 | default: // don't allow duplicate activities 28 | if(details.content){ 29 | dedupDetails.content = details.content.toString(); 30 | } 31 | if(Activities.find(dedupDetails, {limit: 1}).count()){ 32 | return // if this is a duplicate. stop here. 33 | } 34 | 35 | } 36 | 37 | Activities.insert(fullDetails); 38 | }; 39 | 40 | 41 | generateActivityFeedItem = function(userId, activityId, relevancy){ 42 | check(userId, String); 43 | check(activityId, String); 44 | check(relevancy, Date); 45 | 46 | return ActivityFeedItems.insert({ 47 | uId: userId, 48 | aId: activityId, 49 | r: relevancy 50 | }) 51 | }; 52 | 53 | 54 | fanToObject = function(activity){ 55 | check(activity.object, Object); 56 | generateActivityFeedItem(activity.object.id, activity._id, activity.published); 57 | }; 58 | 59 | fanToObjectAuthor = function(activity){ 60 | check(activity.object, Object); 61 | 62 | var populatedObject; 63 | 64 | switch (activity.object.type){ 65 | case 'Story': 66 | populatedObject = Stories.findOne(activity.object.id, {fields: {authorId: 1}}); 67 | break; 68 | default: 69 | throw new Meteor.Error('Object not found in database for activity: ' + activity._id); 70 | } 71 | 72 | if(populatedObject){ 73 | generateActivityFeedItem(populatedObject.authorId, activity._id, activity.published); // fan to author 74 | } 75 | }; 76 | 77 | 78 | fanoutActivity = function(activity){ 79 | check(activity, Object); 80 | check(activity.published, Date); 81 | 82 | Activities.update(activity._id, {$set: {fanout: 'in_progress'}}); 83 | 84 | switch(activity.type){ 85 | case 'Favorite': 86 | fanToObjectAuthor(activity); 87 | break; 88 | case 'Follow': 89 | fanToObject(activity); 90 | sendFollowedYouEmail(activity.object.id, activity.actor.id); 91 | break; 92 | case 'FollowBack': 93 | fanToObject(activity); 94 | sendFollowedYouBackEmail(activity.object.id, activity.actor.id); 95 | break; 96 | case 'Publish': 97 | var author = Meteor.users.findOne(activity.actor.id, {fields: {followers: 1}}); // fan to followers 98 | if(author.followers && author.followers.length){ 99 | _.each(author.followers, function(follower){ 100 | generateActivityFeedItem(follower, activity._id, activity.published); 101 | }); 102 | sendFollowingPublishedEmail(author.followers, activity.object.id); 103 | } 104 | break; 105 | case 'Share': 106 | fanToObjectAuthor(activity); 107 | break; 108 | case 'ViewThreshold': 109 | fanToObjectAuthor(activity); 110 | break; 111 | default: 112 | throw new Error('Activity type not matched for activity: ' + activity._id + ' Type: ' + activity.type); 113 | } 114 | 115 | // if get here, nothing has thrown 116 | return Activities.update(activity._id, {$set: {fanout: 'done'}}); 117 | }; 118 | -------------------------------------------------------------------------------- /server/browser-policy.js: -------------------------------------------------------------------------------- 1 | BrowserPolicy.framing.allowAll(); // allow all sites to embed FOLD 2 | BrowserPolicy.content.disallowInlineScripts(); // this provides a backstop against XSS 3 | BrowserPolicy.content.disallowEval(); // never allow eval 4 | BrowserPolicy.content.allowInlineStyles(); // we use inline styles a fair bit 5 | BrowserPolicy.content.allowImageOrigin('*'); // allowing all images is easiest and seems safe 6 | 7 | // allow videos from specific sources only 8 | BrowserPolicy.content.allowMediaOrigin('res.cloudinary.com'); 9 | BrowserPolicy.content.allowMediaOrigin('*.imgur.com'); 10 | BrowserPolicy.content.allowMediaOrigin('*.giphy.com'); 11 | 12 | // allow iframes from everywhere (needed for various browser bookmarklets) 13 | BrowserPolicy.content.allowFrameOrigin('*'); 14 | 15 | // allow iframes from specific sources only (why not) 16 | BrowserPolicy.content.allowFontOrigin('*.gstatic.com'); 17 | BrowserPolicy.content.allowFontOrigin('*.bootstrapcdn.com'); 18 | 19 | // allow scripts from everywhere (we already don't allow inline above) 20 | BrowserPolicy.content.allowScriptOrigin('*'); 21 | 22 | // allow styles from specific sources only 23 | BrowserPolicy.content.allowStyleOrigin('*.bootstrapcdn.com'); 24 | 25 | // disallow objects (until we need them) 26 | BrowserPolicy.content.disallowObject(); 27 | 28 | // allow connect everywhere 29 | BrowserPolicy.content.allowConnectOrigin('*'); 30 | -------------------------------------------------------------------------------- /server/email.js: -------------------------------------------------------------------------------- 1 | sendWelcomeEmail = function(user){ // this takes actual user instead of userId because user might be in process of being created in db 2 | var email = user.emails[0].address; 3 | var emailName = user.profile.name; 4 | 5 | Mandrill.messages.sendTemplate({ 6 | template_name: 'welcome-e-mail', 7 | template_content: [ 8 | ], 9 | message: { 10 | to: [ 11 | { 12 | email: email, 13 | name: emailName 14 | } 15 | ] 16 | } 17 | }); 18 | }; 19 | 20 | var emailTypeForUnsubscribe = function(emailType){ 21 | switch(emailType){ 22 | case 'followed-you-back': 23 | return 'followed-you' // these are effectively the same 24 | break; 25 | default: 26 | return emailType; 27 | } 28 | }; 29 | 30 | var getToFromUserIds = function(userIds, emailType){ 31 | var unsubscribeCheck = emailTypeForUnsubscribe(emailType); 32 | var users = Meteor.users.find({_id: {$in: userIds}, unsubscribes: {$ne: unsubscribeCheck}}, {fields: {'emails': 1, 'profile.name': 1}}); 33 | return users.map(function(user){ 34 | return { 35 | email: user.emails[0].address, 36 | name: user.profile.name 37 | } 38 | }); 39 | }; 40 | 41 | var getMergeVarsFromObj = function(obj){ 42 | return _.chain(obj) 43 | .pairs() 44 | .map(function(pair){ 45 | return { 46 | name: pair[0], 47 | content: pair[1] 48 | } 49 | }) 50 | .value() 51 | } 52 | 53 | var sendEmail = function(emailType, userIds, subject, bareMergeVars){ 54 | var to = getToFromUserIds(userIds, emailType); 55 | if(to.length === 0){ 56 | return 57 | } 58 | 59 | if(process.env.NODE_ENV === 'production'){ 60 | Mandrill.messages.sendTemplate({ 61 | template_name: emailType, 62 | template_content: [ 63 | ], 64 | message: { 65 | to: to, 66 | subject: subject, 67 | global_merge_vars: getMergeVarsFromObj(_.extend({ unsubscribeUrl: Meteor.absoluteUrl('unsubscribe?email_type=' + emailTypeForUnsubscribe(emailType))}, bareMergeVars)) 68 | }, 69 | preserve_recipients: false 70 | }); 71 | } else { 72 | console.log('Would have sent email') 73 | console.log(arguments) 74 | } 75 | } 76 | 77 | 78 | sendFollowingPublishedEmail = function(userIds, storyId){ 79 | var story = Stories.findOne(storyId, {fields: readStoryFields}); 80 | 81 | var title = story.title; 82 | var authorName = story.authorName; 83 | var longContentPreview = story.contentPreview(); 84 | var subject = authorName + ' just published "' + title + '" on FOLD'; 85 | 86 | var bareMergeVars = {}; 87 | 88 | bareMergeVars.title = title; 89 | bareMergeVars.authorName = authorName; 90 | bareMergeVars.subject = subject; 91 | 92 | bareMergeVars.headerImageUrl = 'https:' + story.headerImageUrl(); 93 | if(longContentPreview){ 94 | bareMergeVars.contentPreview = longContentPreview.length > 203 ? longContentPreview.substring(0, 200).replace(/\s+\S*$/, "...") : longContentPreview; 95 | } 96 | bareMergeVars.profileUrl = Meteor.absoluteUrl('profile/' + (story.authorDisplayUsername || story.authorUsername)); 97 | bareMergeVars.storyUrl = Meteor.absoluteUrl('read/' + story.userPathSegment + '/' + story.storyPathSegment); 98 | 99 | sendEmail('following-published', userIds, subject, bareMergeVars); 100 | }; 101 | 102 | sendFollowedYouEmail = function(userId, followingUserId){ 103 | var followingUser = Meteor.users.findOne(followingUserId, {fields: {'profile.name': 1,'profile.bio': 1,'profile.profilePicture': 1, 'displayUsername': 1, 'services.twitter.id': 1}}); 104 | 105 | var fullName = followingUser.profile.name; // = story.authorName; 106 | var username = followingUser.displayUsername; // = story.authorName; 107 | var subject = fullName + ' (' + username + ') just followed you on FOLD'; 108 | 109 | var bareMergeVars = {}; 110 | 111 | bareMergeVars.fullName = fullName; 112 | bareMergeVars.subject = subject; 113 | bareMergeVars.bio = followingUser.profile.bio || ''; 114 | bareMergeVars.firstName = fullName.split(' ')[0]; 115 | bareMergeVars.profilePicUrl = 'https:' + getProfileImage(followingUser.profile.profilePicture, (followingUser.services && followingUser.services.twitter) ? followingUser.services.twitter.id : null, 'large', true); 116 | bareMergeVars.profileUrl = Meteor.absoluteUrl('profile/' + followingUser.displayUsername); 117 | 118 | 119 | sendEmail('followed-you', [userId], subject, bareMergeVars); 120 | 121 | }; 122 | 123 | sendFollowedYouBackEmail = function(userId, followingUserId){ 124 | var followingUser = Meteor.users.findOne(followingUserId, {fields: {'profile.name': 1,'profile.bio': 1,'profile.profilePicture': 1, 'displayUsername': 1, 'services.twitter.id': 1}}); 125 | 126 | var fullName = followingUser.profile.name; // = story.authorName; 127 | var username = followingUser.displayUsername; // = story.authorName; 128 | var subject = fullName + ' (' + username + ') just followed you back on FOLD'; 129 | 130 | var bareMergeVars = {}; 131 | 132 | bareMergeVars.fullName = fullName; 133 | bareMergeVars.subject = subject; 134 | bareMergeVars.bio = followingUser.profile.bio || ''; 135 | bareMergeVars.firstName = fullName.split(' ')[0]; 136 | bareMergeVars.profilePicUrl = 'https:' + getProfileImage(followingUser.profile.profilePicture, (followingUser.services && followingUser.services.twitter) ? followingUser.services.twitter.id : null, 'large', true); 137 | bareMergeVars.profileUrl = Meteor.absoluteUrl('profile/' + followingUser.displayUsername); 138 | 139 | 140 | sendEmail('followed-you-back', [userId], subject, bareMergeVars); 141 | 142 | }; 143 | 144 | -------------------------------------------------------------------------------- /server/fanout.js: -------------------------------------------------------------------------------- 1 | var runFanout = function (options) { 2 | options = options || {}; 3 | _.defaults(options, {logging: true}); 4 | if(options.logging){ 5 | console.log('Running fanout...'); 6 | } 7 | 8 | var startTime = Date.now(); 9 | var previousTimepoint = Date.now(); 10 | 11 | var timeLogs = []; 12 | 13 | var pendingActivities; 14 | 15 | if (options.cleanup) { 16 | pendingActivities = Activities.find({fanout: "in_progress"}); // find partially fanned out activities 17 | pendingActivities.forEach(function(activity){ 18 | ActivityFeedItems.remove({aId: activity._id}); // remove the related feed items 19 | }); 20 | // then try to fan them out again 21 | 22 | timeLogs.push('in progress activities fetch and activity feed cleanup time: ' + ((Date.now() - previousTimepoint) / 1000) + ' seconds'); 23 | previousTimepoint = Date.now(); 24 | } else { 25 | pendingActivities = Activities.find({fanout: "pending"}); // this is the default 26 | } 27 | 28 | timeLogs.push('pending activities fetch time: ' + ((Date.now() - previousTimepoint) / 1000) + ' seconds'); 29 | previousTimepoint = Date.now(); 30 | 31 | pendingActivities.forEach(fanoutActivity); 32 | timeLogs.push('activity fanout time: ' + ((Date.now() - previousTimepoint) / 1000) + ' seconds'); 33 | previousTimepoint = Date.now(); 34 | 35 | if(options.logging) { 36 | _.each(timeLogs, function (str) { 37 | console.log(str); 38 | }); 39 | 40 | console.log('Total time to run fanout: ' + ((Date.now() - startTime) / 1000) + ' seconds'); 41 | } 42 | 43 | }; 44 | 45 | 46 | var fanOutWaitInSeconds = parseInt(process.env.FANOUT_WAIT) || 5 * 60; // default is every 5 minutes 47 | 48 | 49 | if (process.env.PROCESS_TYPE === 'fanout_worker') { // if a worker process 50 | Meteor.startup(function () { 51 | while (true) { 52 | runFanout(); 53 | Meteor._sleepForMs(fanOutWaitInSeconds * 1000); 54 | } 55 | }); 56 | } else if (process.env.PROCESS_TYPE === 'cleanup_fanout_worker') { // don't run this while fanout worker is running 57 | Meteor.startup(function () { 58 | runFanout({cleanup: true}); 59 | process.exit(); 60 | }); 61 | } else if (process.env.NODE_ENV === 'development') { // however, in developement, run fanout more quickly 62 | Meteor.startup(function () { 63 | var backgroundFanout = function(){ 64 | Meteor.setTimeout(function(){ 65 | runFanout({logging: false}); 66 | backgroundFanout(); 67 | }, 1000); 68 | }; 69 | backgroundFanout(); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | import prerenderIO from 'prerender-node'; 2 | 3 | // If use ssl, will need to check that too 4 | // TO-DO change this to a 301 redirect once totally sure 5 | WebApp.connectHandlers.use(function(req, res, next) { 6 | if (req.method === 'GET' && req.headers.host.match(/^www/) !== null ) { 7 | res.writeHead(307, {Location: 'https://' + req.headers.host.replace(/^www\./, '') + req.url}); 8 | res.end(); 9 | } else { 10 | next(); 11 | } 12 | }); 13 | 14 | if (_.contains([true, 'true'], process.env.ALLOW_BOTS)){ 15 | robots.addLine('User-agent: *\nDisallow: /create/'); 16 | robots.addLine('Disallow: /admin/'); 17 | } else { 18 | robots.addLine('User-agent: *\nDisallow: /'); 19 | } 20 | 21 | WebApp.connectHandlers.use(prerenderIO); 22 | 23 | // if (process.env.PRERENDER_TOKEN) { 24 | // prerenderio.set('prerenderToken', process.env.PRERENDER_TOKEN); 25 | // } 26 | -------------------------------------------------------------------------------- /server/methods.js: -------------------------------------------------------------------------------- 1 | var countStat = function(storyId, stat, details) { 2 | 3 | var connectionId = this.connection.id; 4 | var clientIP = this.connection.httpHeaders['x-forwarded-for'] || this.connection.clientAddress; 5 | 6 | var story = Stories.findOne({_id: storyId, published: true}); 7 | 8 | if (!story){ 9 | throw new Meteor.error('Story not found for count ' + stat + ': ' + storyId); // this mostly confirms the story has been published 10 | } 11 | 12 | var stats = StoryStats.findOne({storyId: storyId}, {fields: {all: 0}}); 13 | 14 | if(!stats){ 15 | stats = {}; 16 | } 17 | 18 | if (!stats.deepAnalytics){ 19 | stats.deepAnalytics= {}; 20 | } 21 | 22 | if (!stats.deepAnalytics[stat]){ 23 | stats.deepAnalytics[stat] = {}; 24 | } 25 | 26 | var addToSet = {}; 27 | var inc = {}; 28 | inc['analytics.' + stat + '.total'] = 1; 29 | 30 | if(!_.contains(stats.deepAnalytics[stat].uniqueViewersByConnection, connectionId)){ 31 | addToSet['deepAnalytics.' + stat + '.uniqueViewersByConnection'] = connectionId ; 32 | inc['analytics.' + stat + '.byConnection'] = 1; 33 | if(stat === 'shares'){ 34 | generateShareActivity(story._id, details.service); 35 | } 36 | } 37 | 38 | if(!_.contains(stats.deepAnalytics[stat].uniqueViewersByIP, clientIP)){ 39 | addToSet['deepAnalytics.' + stat + '.uniqueViewersByIP'] = clientIP ; 40 | inc['analytics.' + stat + '.byIP'] = 1; 41 | if((stat === 'views') && stats.analytics && stats.analytics.views){ 42 | var uniqueViews = stats.analytics.views.byIP + 1; 43 | if(_.contains(VIEW_THRESHOLDS, uniqueViews)){ 44 | generateViewThresholdActivity(story._id, uniqueViews); 45 | } 46 | } 47 | } 48 | 49 | if (this.userId && !_.contains(stats.deepAnalytics[stat].uniqueViewersByUserId, this.userId)){ 50 | addToSet['deepAnalytics.' + stat + '.uniqueViewersByUserId'] = this.userId ; 51 | inc['analytics.' + stat + '.byId'] = 1; 52 | } 53 | 54 | var push = {}; 55 | 56 | var fullData = _.extend({}, _.omit(this.connection, ['close', 'onClose']), {date: new Date}); 57 | 58 | if (this.userId){ 59 | _.extend(fullData, { 60 | userId: this.userId, 61 | username: Meteor.user().username 62 | }); 63 | }; 64 | if (details){ 65 | _.extend(fullData, details); 66 | }; 67 | 68 | push['deepAnalytics.' + stat + '.all'] = fullData; 69 | 70 | Stories.update( {_id: storyId}, {$inc: inc }); 71 | StoryStats.upsert( {storyId: storyId} , {$inc: inc, $addToSet: addToSet, $push: push} ); 72 | }; 73 | 74 | var checkCountMap = function(countMap){ 75 | check(countMap, Object); 76 | _.keys(countMap, function (e) { 77 | check(e, String); // these should be ids 78 | check(e, Match.Where(function (str) { 79 | return (/^[^.]*$/).test(str); // check has no periods 80 | })) 81 | }); 82 | _.values(countMap, function (e) { 83 | check(e, Number); 84 | check(e, Match.Where(function (num) { 85 | return num > 0; // check only positive numbers 86 | })); 87 | }); 88 | }; 89 | 90 | Meteor.methods({ 91 | countStoryView: function(storyId) { 92 | this.unblock(); 93 | check(storyId, String); 94 | countStat.call(this, storyId, 'views'); 95 | }, 96 | countStoryShare: function(storyId, service) { 97 | this.unblock(); 98 | check(storyId, String); 99 | countStat.call(this, storyId, 'shares', {service: service}); 100 | }, 101 | countStoryRead: function(storyId, service) { 102 | this.unblock(); 103 | check(storyId, String); 104 | countStat.call(this, storyId, 'reads', {service: service}); 105 | }, 106 | countStoryAnalytics: function(storyId, analytics) { 107 | this.unblock(); 108 | check(storyId, String); 109 | 110 | var activeHeartbeatCountMap = analytics.activeHeartbeats; 111 | checkCountMap(activeHeartbeatCountMap); 112 | 113 | var anchorClickCountMap = analytics.anchorClicks; 114 | checkCountMap(anchorClickCountMap); 115 | var maxClicks = Math.ceil(activeHeartbeatCountMap.story / 5); 116 | _.keys(anchorClickCountMap, (k) => { 117 | anchorClickCountMap[k] = Math.min(anchorClickCountMap[k], maxClicks) 118 | }); 119 | 120 | var contextInteractionCountMap = analytics.contextInteractions; 121 | checkCountMap(contextInteractionCountMap); 122 | var maxInteractions = Math.ceil(activeHeartbeatCountMap.story / 10); 123 | _.keys(contextInteractionCountMap, (k) => { 124 | contextInteractionCountMap[k] = Math.min(contextInteractionCountMap[k], maxInteractions) 125 | }); 126 | 127 | var incMap = {}; 128 | _.each(_.keys(activeHeartbeatCountMap), function (k) { 129 | incMap['analytics.heartbeats.active.' + k] = activeHeartbeatCountMap[k]; 130 | }); 131 | if(!_.isEmpty(anchorClickCountMap)){ 132 | _.each(_.keys(anchorClickCountMap), function (k) { 133 | incMap['analytics.anchorClicks.' + k] = anchorClickCountMap[k]; 134 | }); 135 | } 136 | if(!_.isEmpty(contextInteractionCountMap)){ 137 | _.each(_.keys(contextInteractionCountMap), function (k) { 138 | incMap['analytics.contextInteractions.' + k] = contextInteractionCountMap[k]; 139 | }); 140 | } 141 | 142 | StoryStats.upsert({storyId: storyId}, {$inc: incMap}); 143 | return Stories.update({_id: storyId}, {$inc: incMap}); 144 | }, 145 | impersonate: function(username) { 146 | check(username, String); 147 | 148 | var user = Meteor.user(); 149 | if (!user || !user.admin || !user.privileges || !user.privileges.impersonation){ 150 | throw new Meteor.Error(403, 'Permission denied'); 151 | } 152 | 153 | var otherUser; 154 | if (!(otherUser = Meteor.users.findOne({username: username}))){ 155 | throw new Meteor.Error(404, 'User not found'); 156 | } 157 | 158 | this.setUserId(otherUser._id); 159 | return otherUser._id 160 | }, 161 | getActivityFeed: function(aId){ 162 | check(aId, Match.Optional(String)); 163 | if(!this.userId){ 164 | throw new Meteor.Error("Only users may get their activity feed"); 165 | } 166 | 167 | var query = aId ? {uId: this.userId, aId: aId} : {uId: this.userId}; 168 | 169 | var activityIds = ActivityFeedItems.find(query, {sort:{r: -1}, limit: 50, fields: {'aId' : 1}}).map(function(i){return i.aId}); 170 | return Activities.find({_id: {$in: activityIds}}).fetch(); 171 | } 172 | }); 173 | -------------------------------------------------------------------------------- /server/search.js: -------------------------------------------------------------------------------- 1 | SearchSource.defineSource('stories', function(searchText, options) { 2 | options = options || {}; 3 | _.defaults(options, { 4 | page: 0 5 | }); 6 | var findOptions = { 7 | sort: [ 8 | ["editorsPickAt", "desc"], 9 | ["favoritedTotal", "desc"], 10 | ["savedAt", "desc"] 11 | ], 12 | limit: PUB_SIZE * (options.page + 1), 13 | fields: previewStoryFields 14 | }; 15 | 16 | if(searchText) { 17 | var regExp = buildRegExp(searchText); 18 | var selector = {$or: [{title: regExp},{ keywords: regExp},{ authorName: regExp},{ authorDisplayUsername: regExp}], 19 | published: true 20 | }; 21 | return Stories.find(selector, findOptions).fetch(); 22 | } else { 23 | return [] 24 | } 25 | }); 26 | 27 | SearchSource.defineSource('people', function(searchText, options) { 28 | options = options || {}; 29 | _.defaults(options, { 30 | page: 0 31 | }); 32 | var findOptions = { 33 | sort: [ 34 | ["followersTotal", "desc"], 35 | ["followingTotal", "desc"], 36 | ["favoritesTotal", "desc"], 37 | ["createdAt", "desc"] 38 | ], 39 | limit: 3 * (options.page + 1), 40 | fields: minimalUserFields 41 | }; 42 | 43 | if(searchText) { 44 | var regExp = buildRegExp(searchText); 45 | var selector = { 46 | username: {$exists: true}, 47 | $or: [{username: regExp},{ 'profile.name': regExp}] 48 | }; 49 | return Meteor.users.find(selector, findOptions).fetch(); 50 | } else { 51 | return [] 52 | } 53 | }); 54 | 55 | function buildRegExp(searchText) { 56 | var words = searchText.trim().split(/[ \-\:]+/); 57 | var exps = _.map(words, function(word) { 58 | return "(?=.*" + word + ")"; 59 | }); 60 | var fullExp = exps.join('') + ".+"; 61 | return new RegExp(fullExp, "i"); 62 | } 63 | -------------------------------------------------------------------------------- /server/settings.js: -------------------------------------------------------------------------------- 1 | // get segment key to the client, while allowing it to be set from environment variable 2 | // NOTE: this hack may not be 100% reliable (for ex when initially deploy won't update clients) 3 | if (process.env.GA_TRACKING_KEY){ 4 | Meteor.settings['public'].GA_TRACKING_KEY = process.env.GA_TRACKING_KEY; 5 | } 6 | 7 | if (process.env.NODE_ENV){ 8 | Meteor.settings['public'].NODE_ENV = process.env.NODE_ENV; 9 | } 10 | 11 | // SMTP Config 12 | smtp = { 13 | username: Meteor.settings.SMTP_USERNAME, 14 | password: Meteor.settings.SMTP_API_KEY, 15 | server: Meteor.settings.SMTP_SERVER, 16 | port: Meteor.settings.SMTP_PORT 17 | }; 18 | 19 | process.env.MAIL_URL = 'smtp://' + encodeURIComponent(smtp.username) + ':' + encodeURIComponent(smtp.password) + '@' + encodeURIComponent(smtp.server) + ':' + smtp.port; 20 | 21 | Mandrill.config({ 22 | key: Meteor.settings.MANDRILL_API_KEY // get your Mandrill key from https://mandrillapp.com/settings/index 23 | }); 24 | 25 | if (Meteor.settings.CLOUDINARY_API_SECRET){ 26 | Cloudinary.config({ 27 | cloud_name: Meteor.settings['public'].CLOUDINARY_CLOUD_NAME, 28 | api_key: Meteor.settings.CLOUDINARY_API_KEY, 29 | api_secret: Meteor.settings.CLOUDINARY_API_SECRET 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /server/user-methods.js: -------------------------------------------------------------------------------- 1 | var TWITTER_API_KEY = process.env.TWITTER_API_KEY || Meteor.settings.TWITTER_API_KEY; 2 | var TWITTER_API_SECRET = process.env.TWITTER_API_SECRET || Meteor.settings.TWITTER_API_SECRET; 3 | 4 | import Twit from 'twit'; 5 | 6 | var makeTwitterCall = function (apiCall, params) { 7 | var res; 8 | var user = Meteor.user(); 9 | var client = new Twit({ 10 | consumer_key: TWITTER_API_KEY, 11 | consumer_secret: TWITTER_API_SECRET, 12 | access_token: user.services.twitter.accessToken, 13 | access_token_secret: user.services.twitter.accessTokenSecret 14 | }); 15 | 16 | var twitterResultsSync = Meteor.wrapAsync(client.get, client); 17 | try { 18 | res = twitterResultsSync(apiCall, params); 19 | } 20 | catch (err) { 21 | if (err.statusCode !== 404) { 22 | throw err; 23 | } 24 | res = {}; 25 | } 26 | return res; 27 | }; 28 | 29 | Meteor.methods({ 30 | updateInitialTwitterUserInfo: function (userInfo) { 31 | check(userInfo, Object); 32 | 33 | var user = Meteor.user(); 34 | if (!user.tempUsername) { 35 | return 36 | } 37 | var username = userInfo.username, 38 | email = userInfo.email; 39 | 40 | if (!email) { 41 | throw new Meteor.Error('Please enter your email'); 42 | } 43 | check(username, String); 44 | check(email, String); 45 | 46 | 47 | checkUserSignup(username, email); 48 | 49 | //get twitter info 50 | var res; 51 | if (user.services.twitter) { 52 | var twitterParams = { 53 | user_id: user.services.twitter.id 54 | }; 55 | try { 56 | res = makeTwitterCall("users/show", twitterParams); 57 | } 58 | catch (err) { 59 | res = {}; 60 | } 61 | } 62 | 63 | var bio = (res && res.description) ? res.description : ""; 64 | 65 | var success = Meteor.users.update({ 66 | _id: this.userId 67 | }, { 68 | $set: { 69 | "profile.name": userInfo.name || username, 70 | "displayUsername": username, 71 | "username": username, 72 | "profile.bio": bio 73 | }, 74 | $unset: {"tempUsername": ""}, 75 | $push: { 76 | "emails": {"address": userInfo.email, "verified": false} 77 | } 78 | }); 79 | 80 | if(success){ 81 | Meteor.defer(() => { 82 | sendWelcomeEmail(Meteor.users.findOne(this.userId)); 83 | }); 84 | } 85 | 86 | return success 87 | }, 88 | setBioFromTwitter: function () { 89 | var user = Meteor.user(); 90 | if (user && user.profile && user.services.twitter) { 91 | var res; 92 | var twitterParams = { 93 | user_id: user.services.twitter.id 94 | }; 95 | res = makeTwitterCall("users/show", twitterParams); 96 | 97 | var bio = res.description; 98 | 99 | if (bio) { 100 | return Meteor.users.update({ 101 | _id: this.userId 102 | }, { 103 | $set: { 104 | "profile.bio": bio 105 | } 106 | }); 107 | } 108 | } 109 | }, 110 | validateUserInfo: function(userInfo){ 111 | check(userInfo.email, String); 112 | userInfo.emails = [{address: userInfo.email}]; 113 | return validateNewUser(userInfo); 114 | }, 115 | unsubscribe (emailType){ 116 | check(emailType, String); 117 | return Meteor.users.update({ 118 | _id: this.userId 119 | }, { 120 | $addToSet: { 121 | "unsubscribes": emailType 122 | } 123 | }); 124 | }, 125 | resubscribe (emailType){ 126 | check(emailType, String); 127 | return Meteor.users.update({ 128 | _id: this.userId 129 | }, { 130 | $pull: { 131 | "unsubscribes": emailType 132 | } 133 | }); 134 | } 135 | }); 136 | -------------------------------------------------------------------------------- /start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | meteor --settings settings.json 3 | exit 4 | --------------------------------------------------------------------------------