├── .dockerignore ├── .gitignore ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── CloudronManifest.json ├── DESCRIPTION.md ├── Dockerfile ├── LICENSE ├── README.md ├── app.js ├── frontend ├── 3rdparty │ ├── css │ │ ├── bootstrap-theme.css │ │ ├── bootstrap-theme.css.map │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap-theme.min.css.map │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ ├── bootstrap.min.css.map │ │ ├── font-awesome.css │ │ └── font-awesome.min.css │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ ├── fontawesome-webfont.woff2 │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ └── js │ │ ├── HtmlSanitizer.js │ │ ├── bootstrap.js │ │ ├── bootstrap.min.js │ │ ├── npm.js │ │ ├── shortcut.js │ │ └── twemoji.min.js ├── error.html ├── favicon.png ├── favicon.svg ├── index.html ├── js │ ├── core.js │ ├── index.js │ ├── shared.js │ ├── stream.js │ └── util.js ├── scss │ ├── index.scss │ ├── markdown.scss │ ├── settings.scss │ └── spinner.scss ├── shared.html ├── stream.html └── templates │ ├── modal-browser-extensions.html │ ├── modal-browser-extensions.js │ ├── modal-cheatsheet.html │ ├── modal-cheatsheet.js │ ├── modal-import.html │ ├── modal-import.js │ ├── modal-settings.html │ ├── modal-settings.js │ ├── navigation-bar.html │ ├── navigation-bar.js │ ├── tag-sidebar.html │ ├── tag-sidebar.js │ ├── thing.html │ ├── thing.js │ ├── view-loading.html │ ├── view-loading.js │ ├── view-login.html │ └── view-login.js ├── gulpfile.js ├── localdevelopment ├── logo.png ├── logo.svg ├── oidc_develop_user_select.html ├── package-lock.json ├── package.json ├── runTests ├── screenshots ├── screenshot0.png ├── screenshot1.png ├── screenshot2.png ├── screenshot3.png └── screenshot4.png ├── src ├── config.js ├── database │ ├── settings.js │ ├── tags.js │ ├── things.js │ └── tokens.js ├── logic.js ├── routes.js ├── test │ └── things-test.js └── users.js ├── start.sh ├── things.json └── webextension ├── detectapp.js ├── logo.svg ├── manifest.json ├── popup.css ├── popup.html ├── popup.js └── shortcut.js /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | .dockerignore 4 | .travis.yml 5 | .jshintrc 6 | node_modules/ 7 | public/ 8 | attachments/ 9 | storage/ 10 | src/test/ 11 | screenshots/ 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | public/ 3 | attachments/ 4 | storage/ 5 | webextension-chrome.zip 6 | webextension-firefox.xpi 7 | .users.json 8 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "unused": true, 5 | "esversion": 6, 6 | "globalstrict": false, 7 | "predef": [ "angular", "$", "describe", "it", "before", "after" ] 8 | } 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "14.15.4" 4 | services: 5 | - mongodb 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | [0.1.2] 2 | * Improved responsive design 3 | 4 | [0.1.3] 5 | * Limit results to 10 and fetch more on scroll 6 | * Support public shared items 7 | * Move tags to search field 8 | 9 | [0.1.4] 10 | * Rework the settings ui 11 | 12 | [0.1.5] 13 | * Improve editing 14 | * Add keyboar shortcuts 15 | 16 | [0.2.0] 17 | * Rework the ui theme 18 | 19 | [0.3.0] 20 | * Fully intergrate with Cloudron usermanagement 21 | 22 | [0.4.0] 23 | * Browser extensions are now in the stores 24 | 25 | [0.4.1] 26 | * Improve performance on external link fetching 27 | 28 | [0.5.0] 29 | * Improve key handling for tags 30 | * Remove some shadows 31 | 32 | [0.5.1] 33 | * Fix bug in hash tag handling with URLs 34 | 35 | [0.6.0] 36 | * Introduce file/image attachments 37 | * Fix focus handling for tags 38 | 39 | [0.6.1] 40 | * Fix image width 41 | * Improve keyboard support 42 | * Add wide layout option 43 | 44 | [0.7.0] 45 | * Refactor code 46 | 47 | [0.7.1] 48 | * Fix bug with subtags 49 | 50 | [0.8.0] 51 | * Email support 52 | 53 | [0.8.1] 54 | * Fix email support 55 | 56 | [0.8.2] 57 | * Add support for Edge and IE 58 | 59 | [0.8.3] 60 | * Allow background images to be uploaded 61 | 62 | [0.8.4] 63 | * Improve edit background 64 | * Support special characters like umlauts in tags 65 | * Improve URL and tag detection 66 | 67 | [0.8.5] 68 | * Open external links in a new tab 69 | 70 | [0.8.6] 71 | * Use new base image 72 | 73 | [0.8.7] 74 | * Support attachments in import/export 75 | 76 | [1.0.0] 77 | * New guacamoly version supporting multiuser 78 | * The first user login will own the previous dataset 79 | 80 | [1.0.1] 81 | * Fix email receiving 82 | * Add busy indicator when fetching more 83 | * Fix tag cleanup job 84 | * Enable welcome notes 85 | 86 | [1.0.2] 87 | * Add text color support 88 | 89 | [1.1.0] 90 | * Add tag proposal on creation and edit of notes 91 | 92 | [1.2.0] 93 | * Support tags starting with numbers 94 | * Add option to disable scroll on edit 95 | * Various bug fixes 96 | 97 | [1.3.0] 98 | * Add public streams 99 | * Improve mobile usage 100 | 101 | [1.3.1] 102 | * First release under the new name meemo 103 | * Fix URL extraction in code blocks 104 | 105 | [1.3.2] 106 | * Fix two crashes 107 | 108 | [1.3.3] 109 | * Add optional most used tags sidebar 110 | 111 | [1.4.0] 112 | * Hide user list on login page 113 | * Support ctrl+f to search within the app 114 | * Support markdown tables 115 | * Update to latest base image 116 | 117 | [1.5.0] 118 | * Add archive functionality 119 | * Better checkbox styling 120 | * Improved markdown cheatsheet 121 | * Improved mobile responsiveness 122 | 123 | [1.5.1] 124 | * Improve archive mode visibility 125 | * Collapse archive and delete buttons 126 | * Fix visibility of notes when edited in feed 127 | 128 | [1.6.0] 129 | * Add support for drag'n'drop of text, images and files 130 | * Add support for pasting content like image buffers 131 | * Open uploaded files in new tab 132 | 133 | [1.6.1] 134 | * Fix copy and paste handling for chrome 135 | 136 | [1.6.2] 137 | * Support inlined checkboxes 138 | 139 | [1.6.3] 140 | * Fix markdown regression 141 | 142 | [1.6.4] 143 | * Always use unique filenames on upload 144 | 145 | [1.6.5] 146 | * Ensure all LDAP queries use bind 147 | 148 | [1.7.0] 149 | * Support sticky notes 150 | 151 | [1.8.0] 152 | * Update project dependencies 153 | * Add file upload progress 154 | 155 | [1.8.1] 156 | * Improve mobile view 157 | 158 | [1.9.0] 159 | * Update to new Cloudron base image 160 | 161 | [1.9.1] 162 | * Update Cloudron screenshot location 163 | 164 | [1.9.2] 165 | * Introduce new icon 166 | * Update sass to be windows compatible 167 | 168 | [1.10.0] 169 | * Rework database logic for better portability 170 | 171 | [1.11.0] 172 | * Remove outdated welcome message 173 | * Remove code to import old data format 174 | * Update meta information 175 | 176 | [1.12.0] 177 | * Remove now obsolete db migration from startup 178 | 179 | [1.12.1] 180 | * Fix Shared link has undefined userId #96 181 | 182 | [1.13.0] 183 | * Update to Cloudron base image v2 184 | * Remove broken mailbox handling 185 | * Fix node module security issues 186 | 187 | [1.13.1] 188 | * Sanitize username input to prevent LDAP DoS attack. Thanks to Alessio (d3lla) for reporting! 189 | * Add forum url 190 | 191 | [1.13.2] 192 | * Update to Meemo 1.14.0 193 | * Update all dependencies 194 | * Update base image to v3 195 | 196 | [1.14.0] 197 | * Update various dependencies 198 | * Fix potential XSS security issue 199 | 200 | [1.15.0] 201 | * Update base image to 3.2.0 202 | 203 | [1.16.0] 204 | * Update dependencies 205 | * Fixup mongodb usage 206 | 207 | [1.16.1] 208 | * Update dependencies 209 | 210 | [1.16.2] 211 | * Update dependencies 212 | * Remove uuid and del node module dependency 213 | 214 | [1.17.0] 215 | * Update base image to 4.0.0 216 | 217 | [1.18.0] 218 | * Use OpenID instead of LDAP for user integration 219 | * Fix public and shared notes pages 220 | 221 | -------------------------------------------------------------------------------- /CloudronManifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "de.nebulon.guacamoly", 3 | "title": "Meemo", 4 | "author": "Johannes Zellner", 5 | "description": "file://DESCRIPTION.md", 6 | "changelog": "file://CHANGELOG.md", 7 | "icon": "file://logo.png", 8 | "tagline": "Your personal notes", 9 | "version": "1.18.0", 10 | "upstreamVersion": "1.18.0", 11 | "healthCheckPath": "/api/healthcheck", 12 | "httpPort": 3000, 13 | "addons": { 14 | "mongodb": {}, 15 | "oidc": { 16 | "loginRedirectUri": "/api/callback" 17 | }, 18 | "localstorage": {} 19 | }, 20 | "tags": ["notes", "bookmarks", "todo", "ideas", "feed", "markdown"], 21 | "manifestVersion": 2, 22 | "minBoxVersion": "7.5.0", 23 | "forumUrl": "https://forum.cloudron.io/category/35/meemo", 24 | "website": "https://meemo.minimal-space.de/", 25 | "documentationUrl": "https://cloudron.io/documentation/apps/meemo/", 26 | "contactEmail": "johannes@nebulon.de", 27 | "mediaLinks": [ 28 | "https://screenshots.cloudron.io/de.nebulon.guacamoly/screenshot0.png", 29 | "https://screenshots.cloudron.io/de.nebulon.guacamoly/screenshot1.png", 30 | "https://screenshots.cloudron.io/de.nebulon.guacamoly/screenshot2.png", 31 | "https://screenshots.cloudron.io/de.nebulon.guacamoly/screenshot3.png", 32 | "https://screenshots.cloudron.io/de.nebulon.guacamoly/screenshot4.png" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /DESCRIPTION.md: -------------------------------------------------------------------------------- 1 | ### Overview 2 | 3 | Meemo is a personal data manager. It lets you simply input any kind of information like notes, thoughts, ideas as well as acts as a bookmarkmanager and todo list. 4 | The user interface resembles a news feed organized with tags. Full text search further allows you to quickly find information in your pile of accumulated data. 5 | 6 | ### Features 7 | * Hashtag style categories 8 | * Full text search 9 | * Markdown support 10 | * Data import and export 11 | 12 | ### Email 13 | 14 | Meemo will automatically add notes if you send it email! 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cloudron/base:4.2.0@sha256:46da2fffb36353ef714f97ae8e962bd2c212ca091108d768ba473078319a47f4 2 | 3 | RUN mkdir -p /app/code 4 | WORKDIR /app/code 5 | 6 | ARG VERSION=1.13.0 7 | 8 | ADD src/ /app/code/src/ 9 | ADD frontend/ /app/code/frontend/ 10 | ADD gulpfile.js package.json package-lock.json app.js start.sh things.json logo.png logo.svg /app/code/ 11 | 12 | RUN npm install && npm install -g gulp-cli && gulp default --revision ${VERSION} 13 | 14 | CMD [ "/app/code/start.sh" ] 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Cloudron UG 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Development has moved to : https://git.cloudron.io/packages/meemo-app** 2 | 3 | # Meemo 4 | 5 | Meemo is a personal data manager. It lets you simply input any kind of information like notes, thoughts, ideas as well as acts as a bookmarkmanager and todo list. 6 | The user interface resembles a news feed organized with tags. Full text search further allows you to quickly find information in your pile of accumulated data. 7 | 8 | For better bookmarking, there are chrome and firefox webextensions available. 9 | 10 | ## Installation 11 | 12 | [![Install](https://cloudron.io/img/button32.png)](https://cloudron.io/button.html?app=de.nebulon.guacamoly) 13 | 14 | or using the [Cloudron command line tooling](https://cloudron.io/references/cli.html) 15 | 16 | ``` 17 | cloudron install --appstore-id de.nebulon.guacamoly 18 | ``` 19 | 20 | ## Building 21 | 22 | The app package can be built using the [Cloudron command line tooling](https://cloudron.io/references/cli.html). 23 | 24 | ``` 25 | cd meemo 26 | 27 | cloudron build 28 | cloudron install 29 | ``` 30 | 31 | ## Development 32 | 33 | The app can also be run locally for development. 34 | 35 | ``` 36 | cd meemo 37 | 38 | npm install 39 | gulp # or gulp develop in a new terminal 40 | ./app.js 41 | ``` 42 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | require('supererror')({ splatchError: true }); 6 | 7 | const PORT = process.env.VITE_DEV_PORT || process.env.PORT || 3000; 8 | const BIND_ADDRESS = process.env.BIND_ADDRESS || '0.0.0.0'; 9 | 10 | if (!process.env.CLOUDRON_APP_ORIGIN) { 11 | console.log('No CLOUDRON_APP_ORIGIN env var set. Falling back to http://localhost'); 12 | } 13 | 14 | const APP_ORIGIN = process.env.CLOUDRON_APP_ORIGIN || `http://localhost:${PORT}`; 15 | 16 | var express = require('express'), 17 | json = require('body-parser').json, 18 | config = require('./src/config.js'), 19 | cors = require('cors'), 20 | session = require('express-session'), 21 | MongoStore = require('connect-mongo'), 22 | multer = require('multer'), 23 | oidc = require('express-openid-connect'), 24 | routes = require('./src/routes.js'), 25 | lastmile = require('connect-lastmile'), 26 | logic = require('./src/logic.js'), 27 | MongoClient = require('mongodb').MongoClient, 28 | morgan = require('morgan'), 29 | path = require('path'), 30 | serveStatic = require('serve-static'); 31 | 32 | var app = express(); 33 | var router = new express.Router(); 34 | 35 | var storage = multer.diskStorage({}); 36 | var diskUpload = multer({ storage: storage }).any(); 37 | var memoryUpload = multer({ storage: multer.memoryStorage({}) }).any(); 38 | 39 | router.del = router.delete; 40 | 41 | router.get ('/api/login', function (req, res) { 42 | res.oidc.login({ 43 | returnTo: '/', 44 | authorizationParams: { 45 | redirect_uri: `${APP_ORIGIN}/api/callback`, 46 | } 47 | }); 48 | }); 49 | 50 | router.post('/api/things', routes.auth, routes.add); 51 | router.get ('/api/things', routes.auth, routes.getAll); 52 | router.get ('/api/things/:id', routes.auth, routes.get); 53 | router.put ('/api/things/:id', routes.auth, routes.put); 54 | router.del ('/api/things/:id', routes.auth, routes.del); 55 | 56 | router.post('/api/files', routes.auth, memoryUpload, routes.fileAdd); 57 | router.get ('/api/files/:userId/:thingId/:identifier', routes.fileGet); 58 | 59 | router.get ('/api/tags', routes.auth, routes.getTags); 60 | 61 | router.post('/api/settings', routes.auth, routes.settingsSave); 62 | router.get ('/api/settings', routes.auth, routes.settingsGet); 63 | 64 | router.get ('/api/export', routes.auth, routes.exportThings); 65 | router.post('/api/import', routes.auth, diskUpload, routes.importThings); 66 | 67 | router.get ('/api/profile', routes.auth, routes.profile); 68 | 69 | // public apis 70 | router.get ('/api/public/:userId/files/:fileId', routes.public.getFile); 71 | router.get ('/api/public/:userId/things', routes.public.getAll); 72 | router.get ('/api/public/:userId/things/:thingId', routes.public.getThing); 73 | router.get ('/api/rss/:userId', routes.public.getRSS); 74 | 75 | router.get ('/api/users', routes.public.users); 76 | router.get ('/api/users/:userId', routes.public.profile); 77 | 78 | router.get ('/api/healthcheck', routes.healthcheck); 79 | 80 | // page overlay for pretty public streams 81 | router.get ('/public/:userId', routes.public.streamPage); 82 | 83 | // Add pretty 404 handler 84 | router.get ('*', function (req, res) { 85 | res.sendFile(path.resolve(__dirname, 'public/error.html')); 86 | }); 87 | 88 | if (process.env.DEBUG) { 89 | app.use(morgan('dev', { immediate: false, stream: { write: function (str) { console.log(str.slice(0, -1)); } } })); 90 | } 91 | 92 | app.use(serveStatic(__dirname + '/public', { etag: false })); 93 | app.use(cors()); 94 | app.use(json({ strict: true, limit: '5mb' })); 95 | app.use(session({ 96 | secret: 'guacamoly should be', 97 | saveUninitialized: false, 98 | resave: false, 99 | cookie: { sameSite: 'strict' }, 100 | store: MongoStore.create({ mongoUrl: config.databaseUrl }) 101 | })); 102 | 103 | if (process.env.CLOUDRON_OIDC_ISSUER) { 104 | console.log('Using Cloudron OpenID integration'); 105 | app.use(oidc.auth({ 106 | issuerBaseURL: process.env.CLOUDRON_OIDC_ISSUER, 107 | baseURL: APP_ORIGIN, 108 | clientID: process.env.CLOUDRON_OIDC_CLIENT_ID, 109 | clientSecret: process.env.CLOUDRON_OIDC_CLIENT_SECRET, 110 | secret: 'FIXME this secret', 111 | authorizationParams: { 112 | response_type: 'code', 113 | scope: 'openid profile email' 114 | }, 115 | authRequired: false, 116 | routes: { 117 | callback: '/api/callback', 118 | login: false, 119 | logout: '/api/logout' 120 | }, 121 | session: { 122 | name: 'MeemoSession', 123 | rolling: true, 124 | rollingDuration: 24 * 60 * 60 * 4 // max 4 days idling 125 | } 126 | })); 127 | } else { 128 | // mock oidc 129 | console.log('CLOUDRON_OIDC_ISSUER is not set, using mock OpenID for development'); 130 | 131 | app.use((req, res, next) => { 132 | res.oidc = { 133 | login(options) { 134 | res.writeHead(200, { 'Content-Type': 'text/html' }) 135 | res.write(require('fs').readFileSync(__dirname + '/oidc_develop_user_select.html', 'utf8').replaceAll('REDIRECT_URI', options.authorizationParams.redirect_uri)); 136 | res.end() 137 | } 138 | }; 139 | 140 | req.oidc = { 141 | user: {}, 142 | isAuthenticated() { 143 | return !!req.session.username; 144 | } 145 | }; 146 | 147 | if (req.session.username) { 148 | req.oidc.user = { 149 | sub: req.session.username, 150 | family_name: 'Cloudron', 151 | given_name: req.session.username.toUpperCase(), 152 | locale: 'en-US', 153 | name: 'Cloudron ' + req.session.username.toUpperCase(), 154 | preferred_username: req.session.username, 155 | email: req.session.username + '@cloudron.local', 156 | email_verified: true 157 | }; 158 | } 159 | 160 | next(); 161 | }); 162 | 163 | app.use('/api/callback', (req, res) => { 164 | req.session.username = req.query.username; 165 | res.redirect(`http://localhost:${PORT}/`); 166 | }); 167 | 168 | app.use('/api/logout', (req, res) => { 169 | req.session.username = null; 170 | res.status(200).send({}); 171 | }); 172 | } 173 | 174 | app.use(router); 175 | app.use(lastmile()); 176 | 177 | function exit(error) { 178 | if (error) console.error(error); 179 | process.exit(error ? 1 : 0); 180 | } 181 | 182 | MongoClient.connect(config.databaseUrl, { useUnifiedTopology: true }, function (error, client) { 183 | if (error) exit(error); 184 | 185 | // stash for database code to be used 186 | config.db = client.db(); 187 | 188 | var server = app.listen(PORT, BIND_ADDRESS, function () { 189 | var host = server.address().address; 190 | var port = server.address().port; 191 | 192 | console.log('App listening at http://%s:%s', host, port); 193 | 194 | setInterval(logic.cleanupTags, 1000 * 60); 195 | }); 196 | }); 197 | -------------------------------------------------------------------------------- /frontend/3rdparty/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudron-io/meemo/0ccde1f7f5104c33dcc80053823fa44183756438/frontend/3rdparty/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /frontend/3rdparty/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudron-io/meemo/0ccde1f7f5104c33dcc80053823fa44183756438/frontend/3rdparty/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /frontend/3rdparty/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudron-io/meemo/0ccde1f7f5104c33dcc80053823fa44183756438/frontend/3rdparty/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /frontend/3rdparty/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudron-io/meemo/0ccde1f7f5104c33dcc80053823fa44183756438/frontend/3rdparty/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /frontend/3rdparty/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudron-io/meemo/0ccde1f7f5104c33dcc80053823fa44183756438/frontend/3rdparty/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /frontend/3rdparty/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudron-io/meemo/0ccde1f7f5104c33dcc80053823fa44183756438/frontend/3rdparty/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /frontend/3rdparty/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudron-io/meemo/0ccde1f7f5104c33dcc80053823fa44183756438/frontend/3rdparty/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /frontend/3rdparty/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudron-io/meemo/0ccde1f7f5104c33dcc80053823fa44183756438/frontend/3rdparty/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /frontend/3rdparty/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudron-io/meemo/0ccde1f7f5104c33dcc80053823fa44183756438/frontend/3rdparty/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /frontend/3rdparty/js/HtmlSanitizer.js: -------------------------------------------------------------------------------- 1 | //JavaScript HTML Sanitizer, (c) Alexander Yumashev, Jitbit Software. 2 | 3 | //homepage https://github.com/jitbit/HtmlSanitizer 4 | 5 | //License: MIT https://github.com/jitbit/HtmlSanitizer/blob/master/LICENSE 6 | 7 | console.log('Sanitizer loading'); 8 | 9 | var HtmlSanitizer = new (function () { 10 | 11 | var tagWhitelist_ = { 12 | 'A': true, 'ABBR': true, 'B': true, 'BLOCKQUOTE': true, 'BODY': true, 'BR': true, 'CENTER': true, 'CODE': true, 'DIV': true, 'EM': true, 'FONT': true, 13 | 'H1': true, 'H2': true, 'H3': true, 'H4': true, 'H5': true, 'H6': true, 'HR': true, 'I': true, 'IMG': true, 'LABEL': true, 'LI': true, 'OL': true, 'P': true, 'PRE': true, 14 | 'SMALL': true, 'SOURCE': true, 'SPAN': true, 'STRONG': true, 'TABLE': true, 'TBODY': true, 'TR': true, 'TD': true, 'TH': true, 'THEAD': true, 'UL': true, 'U': true, 'VIDEO': true 15 | }; 16 | 17 | var contentTagWhiteList_ = { 'FORM': true }; //tags that will be converted to DIVs 18 | 19 | var attributeWhitelist_ = { 'align': true, 'color': true, 'controls': true, 'height': true, 'href': true, 'src': true, 'style': true, 'target': true, 'title': true, 'type': true, 'width': true }; 20 | 21 | var cssWhitelist_ = { 'color': true, 'background-color': true, 'font-size': true, 'text-align': true, 'text-decoration': true, 'font-weight': true }; 22 | 23 | var schemaWhiteList_ = [ 'http:', 'https:', 'data:', 'm-files:', 'file:', 'ftp:' ]; //which "protocols" are allowed in "href", "src" etc 24 | 25 | var uriAttributes_ = { 'href': true, 'action': true }; 26 | 27 | this.SanitizeHtml = function(input) { 28 | input = input.trim(); 29 | if (input == "") return ""; //to save performance and not create iframe 30 | 31 | //firefox "bogus node" workaround 32 | if (input == "
") return ""; 33 | 34 | var iframe = document.createElement('iframe'); 35 | if (iframe['sandbox'] === undefined) { 36 | alert('Your browser does not support sandboxed iframes. Please upgrade to a modern browser.'); 37 | return ''; 38 | } 39 | iframe['sandbox'] = 'allow-same-origin'; 40 | iframe.style.display = 'none'; 41 | document.body.appendChild(iframe); // necessary so the iframe contains a document 42 | var iframedoc = iframe.contentDocument || iframe.contentWindow.document; 43 | if (iframedoc.body == null) iframedoc.write(""); // null in IE 44 | iframedoc.body.innerHTML = input; 45 | 46 | function makeSanitizedCopy(node) { 47 | if (node.nodeType == Node.TEXT_NODE) { 48 | var newNode = node.cloneNode(true); 49 | } else if (node.nodeType == Node.ELEMENT_NODE && (tagWhitelist_[node.tagName] || contentTagWhiteList_[node.tagName])) { 50 | 51 | //remove useless empty spans (lots of those when pasting from MS Outlook) 52 | if ((node.tagName == "SPAN" || node.tagName == "B" || node.tagName == "I" || node.tagName == "U") 53 | && node.innerHTML.trim() == "") { 54 | return document.createDocumentFragment(); 55 | } 56 | 57 | if (contentTagWhiteList_[node.tagName]) 58 | newNode = iframedoc.createElement('DIV'); //convert to DIV 59 | else 60 | newNode = iframedoc.createElement(node.tagName); 61 | 62 | for (var i = 0; i < node.attributes.length; i++) { 63 | var attr = node.attributes[i]; 64 | if (attributeWhitelist_[attr.name]) { 65 | if (attr.name == "style") { 66 | for (s = 0; s < node.style.length; s++) { 67 | var styleName = node.style[s]; 68 | if (cssWhitelist_[styleName]) 69 | newNode.style.setProperty(styleName, node.style.getPropertyValue(styleName)); 70 | } 71 | } 72 | else { 73 | if (uriAttributes_[attr.name]) { //if this is a "uri" attribute, that can have "javascript:" or something 74 | if (attr.value.indexOf(":") > -1 && !startsWithAny(attr.value, schemaWhiteList_)) 75 | continue; 76 | } 77 | newNode.setAttribute(attr.name, attr.value); 78 | } 79 | } 80 | } 81 | for (i = 0; i < node.childNodes.length; i++) { 82 | var subCopy = makeSanitizedCopy(node.childNodes[i]); 83 | newNode.appendChild(subCopy, false); 84 | } 85 | } else { 86 | newNode = document.createDocumentFragment(); 87 | } 88 | return newNode; 89 | }; 90 | 91 | var resultElement = makeSanitizedCopy(iframedoc.body); 92 | document.body.removeChild(iframe); 93 | return resultElement.innerHTML 94 | .replace(/]*>(\S)/g, "
\n$1") 95 | .replace(/div>
\n", 74 | "/":"?", 75 | "\\":"|" 76 | } 77 | //Special Keys - and their codes 78 | var special_keys = { 79 | 'esc':27, 80 | 'escape':27, 81 | 'tab':9, 82 | 'space':32, 83 | 'return':13, 84 | 'enter':13, 85 | 'backspace':8, 86 | 87 | 'scrolllock':145, 88 | 'scroll_lock':145, 89 | 'scroll':145, 90 | 'capslock':20, 91 | 'caps_lock':20, 92 | 'caps':20, 93 | 'numlock':144, 94 | 'num_lock':144, 95 | 'num':144, 96 | 97 | 'pause':19, 98 | 'break':19, 99 | 100 | 'insert':45, 101 | 'home':36, 102 | 'delete':46, 103 | 'end':35, 104 | 105 | 'pageup':33, 106 | 'page_up':33, 107 | 'pu':33, 108 | 109 | 'pagedown':34, 110 | 'page_down':34, 111 | 'pd':34, 112 | 113 | 'left':37, 114 | 'up':38, 115 | 'right':39, 116 | 'down':40, 117 | 118 | 'f1':112, 119 | 'f2':113, 120 | 'f3':114, 121 | 'f4':115, 122 | 'f5':116, 123 | 'f6':117, 124 | 'f7':118, 125 | 'f8':119, 126 | 'f9':120, 127 | 'f10':121, 128 | 'f11':122, 129 | 'f12':123 130 | } 131 | 132 | var modifiers = { 133 | shift: { wanted:false, pressed:false}, 134 | ctrl : { wanted:false, pressed:false}, 135 | alt : { wanted:false, pressed:false}, 136 | meta : { wanted:false, pressed:false} //Meta is Mac specific 137 | }; 138 | 139 | if(e.ctrlKey) modifiers.ctrl.pressed = true; 140 | if(e.shiftKey) modifiers.shift.pressed = true; 141 | if(e.altKey) modifiers.alt.pressed = true; 142 | if(e.metaKey) modifiers.meta.pressed = true; 143 | 144 | for(var i=0; k=keys[i],i 1) { //If it is a special key 161 | if(special_keys[k] == code) kp++; 162 | 163 | } else if(opt['keycode']) { 164 | if(opt['keycode'] == code) kp++; 165 | 166 | } else { //The special keys did not match 167 | if(character == k) kp++; 168 | else { 169 | if(shift_nums[character] && e.shiftKey) { //Stupid Shift key bug created by using lowercase 170 | character = shift_nums[character]; 171 | if(character == k) kp++; 172 | } 173 | } 174 | } 175 | } 176 | 177 | if(kp == keys.length && 178 | modifiers.ctrl.pressed == modifiers.ctrl.wanted && 179 | modifiers.shift.pressed == modifiers.shift.wanted && 180 | modifiers.alt.pressed == modifiers.alt.wanted && 181 | modifiers.meta.pressed == modifiers.meta.wanted) { 182 | callback(e); 183 | 184 | if(!opt['propagate']) { //Stop the event 185 | //e.cancelBubble is supported by IE - this will kill the bubbling process. 186 | e.cancelBubble = true; 187 | e.returnValue = false; 188 | 189 | //e.stopPropagation works in Firefox. 190 | if (e.stopPropagation) { 191 | e.stopPropagation(); 192 | e.preventDefault(); 193 | } 194 | return false; 195 | } 196 | } 197 | } 198 | this.all_shortcuts[shortcut_combination] = { 199 | 'callback':func, 200 | 'target':ele, 201 | 'event': opt['type'] 202 | }; 203 | //Attach the function with the event 204 | if(ele.addEventListener) ele.addEventListener(opt['type'], func, false); 205 | else if(ele.attachEvent) ele.attachEvent('on'+opt['type'], func); 206 | else ele['on'+opt['type']] = func; 207 | }, 208 | 209 | //Remove the shortcut - just specify the shortcut and I will remove the binding 210 | 'remove':function(shortcut_combination) { 211 | shortcut_combination = shortcut_combination.toLowerCase(); 212 | var binding = this.all_shortcuts[shortcut_combination]; 213 | delete(this.all_shortcuts[shortcut_combination]) 214 | if(!binding) return; 215 | var type = binding['event']; 216 | var ele = binding['target']; 217 | var callback = binding['callback']; 218 | 219 | if(ele.detachEvent) ele.detachEvent('on'+type, callback); 220 | else if(ele.removeEventListener) ele.removeEventListener(type, callback, false); 221 | else ele['on'+type] = false; 222 | } 223 | } -------------------------------------------------------------------------------- /frontend/3rdparty/js/twemoji.min.js: -------------------------------------------------------------------------------- 1 | /*! Copyright Twitter Inc. and other contributors. Licensed under MIT */ 2 | var twemoji=function(){"use strict";var twemoji={base:(location.protocol==="https:"?"https:":"http:")+"//twemoji.maxcdn.com/",ext:".png",size:"36x36",className:"emoji",convert:{fromCodePoint:fromCodePoint,toCodePoint:toCodePoint},onerror:function onerror(){if(this.parentNode){this.parentNode.replaceChild(createText(this.alt),this)}},parse:parse,replace:replace,test:test},escaper={"&":"&","<":"<",">":">","'":"'",'"':"""},re=/((?:\ud83c\udde8\ud83c\uddf3|\ud83c\uddfa\ud83c\uddf8|\ud83c\uddf7\ud83c\uddfa|\ud83c\uddf0\ud83c\uddf7|\ud83c\uddef\ud83c\uddf5|\ud83c\uddee\ud83c\uddf9|\ud83c\uddec\ud83c\udde7|\ud83c\uddeb\ud83c\uddf7|\ud83c\uddea\ud83c\uddf8|\ud83c\udde9\ud83c\uddea|\u0039\ufe0f?\u20e3|\u0038\ufe0f?\u20e3|\u0037\ufe0f?\u20e3|\u0036\ufe0f?\u20e3|\u0035\ufe0f?\u20e3|\u0034\ufe0f?\u20e3|\u0033\ufe0f?\u20e3|\u0032\ufe0f?\u20e3|\u0031\ufe0f?\u20e3|\u0030\ufe0f?\u20e3|\u0023\ufe0f?\u20e3|\ud83d\udeb3|\ud83d\udeb1|\ud83d\udeb0|\ud83d\udeaf|\ud83d\udeae|\ud83d\udea6|\ud83d\udea3|\ud83d\udea1|\ud83d\udea0|\ud83d\ude9f|\ud83d\ude9e|\ud83d\ude9d|\ud83d\ude9c|\ud83d\ude9b|\ud83d\ude98|\ud83d\ude96|\ud83d\ude94|\ud83d\ude90|\ud83d\ude8e|\ud83d\ude8d|\ud83d\ude8b|\ud83d\ude8a|\ud83d\ude88|\ud83d\ude86|\ud83d\ude82|\ud83d\ude81|\ud83d\ude36|\ud83d\ude34|\ud83d\ude2f|\ud83d\ude2e|\ud83d\ude2c|\ud83d\ude27|\ud83d\ude26|\ud83d\ude1f|\ud83d\ude1b|\ud83d\ude19|\ud83d\ude17|\ud83d\ude15|\ud83d\ude11|\ud83d\ude10|\ud83d\ude0e|\ud83d\ude08|\ud83d\ude07|\ud83d\ude00|\ud83d\udd67|\ud83d\udd66|\ud83d\udd65|\ud83d\udd64|\ud83d\udd63|\ud83d\udd62|\ud83d\udd61|\ud83d\udd60|\ud83d\udd5f|\ud83d\udd5e|\ud83d\udd5d|\ud83d\udd5c|\ud83d\udd2d|\ud83d\udd2c|\ud83d\udd15|\ud83d\udd09|\ud83d\udd08|\ud83d\udd07|\ud83d\udd06|\ud83d\udd05|\ud83d\udd04|\ud83d\udd02|\ud83d\udd01|\ud83d\udd00|\ud83d\udcf5|\ud83d\udcef|\ud83d\udced|\ud83d\udcec|\ud83d\udcb7|\ud83d\udcb6|\ud83d\udcad|\ud83d\udc6d|\ud83d\udc6c|\ud83d\udc65|\ud83d\udc2a|\ud83d\udc16|\ud83d\udc15|\ud83d\udc13|\ud83d\udc10|\ud83d\udc0f|\ud83d\udc0b|\ud83d\udc0a|\ud83d\udc09|\ud83d\udc08|\ud83d\udc07|\ud83d\udc06|\ud83d\udc05|\ud83d\udc04|\ud83d\udc03|\ud83d\udc02|\ud83d\udc01|\ud83d\udc00|\ud83c\udfe4|\ud83c\udfc9|\ud83c\udfc7|\ud83c\udf7c|\ud83c\udf50|\ud83c\udf4b|\ud83c\udf33|\ud83c\udf32|\ud83c\udf1e|\ud83c\udf1d|\ud83c\udf1c|\ud83c\udf1a|\ud83c\udf18|\ud83c\udccf|\ud83c\udd8e|\ud83c\udd91|\ud83c\udd92|\ud83c\udd93|\ud83c\udd94|\ud83c\udd95|\ud83c\udd96|\ud83c\udd97|\ud83c\udd98|\ud83c\udd99|\ud83c\udd9a|\ud83d\udc77|\ud83d\udec5|\ud83d\udec4|\ud83d\udec3|\ud83d\udec2|\ud83d\udec1|\ud83d\udebf|\ud83d\udeb8|\ud83d\udeb7|\ud83d\udeb5|\ud83c\ude01|\ud83c\ude32|\ud83c\ude33|\ud83c\ude34|\ud83c\ude35|\ud83c\ude36|\ud83c\ude38|\ud83c\ude39|\ud83c\ude3a|\ud83c\ude50|\ud83c\ude51|\ud83c\udf00|\ud83c\udf01|\ud83c\udf02|\ud83c\udf03|\ud83c\udf04|\ud83c\udf05|\ud83c\udf06|\ud83c\udf07|\ud83c\udf08|\ud83c\udf09|\ud83c\udf0a|\ud83c\udf0b|\ud83c\udf0c|\ud83c\udf0f|\ud83c\udf11|\ud83c\udf13|\ud83c\udf14|\ud83c\udf15|\ud83c\udf19|\ud83c\udf1b|\ud83c\udf1f|\ud83c\udf20|\ud83c\udf30|\ud83c\udf31|\ud83c\udf34|\ud83c\udf35|\ud83c\udf37|\ud83c\udf38|\ud83c\udf39|\ud83c\udf3a|\ud83c\udf3b|\ud83c\udf3c|\ud83c\udf3d|\ud83c\udf3e|\ud83c\udf3f|\ud83c\udf40|\ud83c\udf41|\ud83c\udf42|\ud83c\udf43|\ud83c\udf44|\ud83c\udf45|\ud83c\udf46|\ud83c\udf47|\ud83c\udf48|\ud83c\udf49|\ud83c\udf4a|\ud83c\udf4c|\ud83c\udf4d|\ud83c\udf4e|\ud83c\udf4f|\ud83c\udf51|\ud83c\udf52|\ud83c\udf53|\ud83c\udf54|\ud83c\udf55|\ud83c\udf56|\ud83c\udf57|\ud83c\udf58|\ud83c\udf59|\ud83c\udf5a|\ud83c\udf5b|\ud83c\udf5c|\ud83c\udf5d|\ud83c\udf5e|\ud83c\udf5f|\ud83c\udf60|\ud83c\udf61|\ud83c\udf62|\ud83c\udf63|\ud83c\udf64|\ud83c\udf65|\ud83c\udf66|\ud83c\udf67|\ud83c\udf68|\ud83c\udf69|\ud83c\udf6a|\ud83c\udf6b|\ud83c\udf6c|\ud83c\udf6d|\ud83c\udf6e|\ud83c\udf6f|\ud83c\udf70|\ud83c\udf71|\ud83c\udf72|\ud83c\udf73|\ud83c\udf74|\ud83c\udf75|\ud83c\udf76|\ud83c\udf77|\ud83c\udf78|\ud83c\udf79|\ud83c\udf7a|\ud83c\udf7b|\ud83c\udf80|\ud83c\udf81|\ud83c\udf82|\ud83c\udf83|\ud83c\udf84|\ud83c\udf85|\ud83c\udf86|\ud83c\udf87|\ud83c\udf88|\ud83c\udf89|\ud83c\udf8a|\ud83c\udf8b|\ud83c\udf8c|\ud83c\udf8d|\ud83c\udf8e|\ud83c\udf8f|\ud83c\udf90|\ud83c\udf91|\ud83c\udf92|\ud83c\udf93|\ud83c\udfa0|\ud83c\udfa1|\ud83c\udfa2|\ud83c\udfa3|\ud83c\udfa4|\ud83c\udfa5|\ud83c\udfa6|\ud83c\udfa7|\ud83c\udfa8|\ud83c\udfa9|\ud83c\udfaa|\ud83c\udfab|\ud83c\udfac|\ud83c\udfad|\ud83c\udfae|\ud83c\udfaf|\ud83c\udfb0|\ud83c\udfb1|\ud83c\udfb2|\ud83c\udfb3|\ud83c\udfb4|\ud83c\udfb5|\ud83c\udfb6|\ud83c\udfb7|\ud83c\udfb8|\ud83c\udfb9|\ud83c\udfba|\ud83c\udfbb|\ud83c\udfbc|\ud83c\udfbd|\ud83c\udfbe|\ud83c\udfbf|\ud83c\udfc0|\ud83c\udfc1|\ud83c\udfc2|\ud83c\udfc3|\ud83c\udfc4|\ud83c\udfc6|\ud83c\udfc8|\ud83c\udfca|\ud83c\udfe0|\ud83c\udfe1|\ud83c\udfe2|\ud83c\udfe3|\ud83c\udfe5|\ud83c\udfe6|\ud83c\udfe7|\ud83c\udfe8|\ud83c\udfe9|\ud83c\udfea|\ud83c\udfeb|\ud83c\udfec|\ud83c\udfed|\ud83c\udfee|\ud83c\udfef|\ud83c\udff0|\ud83d\udc0c|\ud83d\udc0d|\ud83d\udc0e|\ud83d\udc11|\ud83d\udc12|\ud83d\udc14|\ud83d\udc17|\ud83d\udc18|\ud83d\udc19|\ud83d\udc1a|\ud83d\udc1b|\ud83d\udc1c|\ud83d\udc1d|\ud83d\udc1e|\ud83d\udc1f|\ud83d\udc20|\ud83d\udc21|\ud83d\udc22|\ud83d\udc23|\ud83d\udc24|\ud83d\udc25|\ud83d\udc26|\ud83d\udc27|\ud83d\udc28|\ud83d\udc29|\ud83d\udc2b|\ud83d\udc2c|\ud83d\udc2d|\ud83d\udc2e|\ud83d\udc2f|\ud83d\udc30|\ud83d\udc31|\ud83d\udc32|\ud83d\udc33|\ud83d\udc34|\ud83d\udc35|\ud83d\udc36|\ud83d\udc37|\ud83d\udc38|\ud83d\udc39|\ud83d\udc3a|\ud83d\udc3b|\ud83d\udc3c|\ud83d\udc3d|\ud83d\udc3e|\ud83d\udc40|\ud83d\udc42|\ud83d\udc43|\ud83d\udc44|\ud83d\udc45|\ud83d\udc46|\ud83d\udc47|\ud83d\udc48|\ud83d\udc49|\ud83d\udc4a|\ud83d\udc4b|\ud83d\udc4c|\ud83d\udc4d|\ud83d\udc4e|\ud83d\udc4f|\ud83d\udc50|\ud83d\udc51|\ud83d\udc52|\ud83d\udc53|\ud83d\udc54|\ud83d\udc55|\ud83d\udc56|\ud83d\udc57|\ud83d\udc58|\ud83d\udc59|\ud83d\udc5a|\ud83d\udc5b|\ud83d\udc5c|\ud83d\udc5d|\ud83d\udc5e|\ud83d\udc5f|\ud83d\udc60|\ud83d\udc61|\ud83d\udc62|\ud83d\udc63|\ud83d\udc64|\ud83d\udc66|\ud83d\udc67|\ud83d\udc68|\ud83d\udc69|\ud83d\udc6a|\ud83d\udc6b|\ud83d\udc6e|\ud83d\udc6f|\ud83d\udc70|\ud83d\udc71|\ud83d\udc72|\ud83d\udc73|\ud83d\udc74|\ud83d\udc75|\ud83d\udc76|\ud83d\udeb4|\ud83d\udc78|\ud83d\udc79|\ud83d\udc7a|\ud83d\udc7b|\ud83d\udc7c|\ud83d\udc7d|\ud83d\udc7e|\ud83d\udc7f|\ud83d\udc80|\ud83d\udc81|\ud83d\udc82|\ud83d\udc83|\ud83d\udc84|\ud83d\udc85|\ud83d\udc86|\ud83d\udc87|\ud83d\udc88|\ud83d\udc89|\ud83d\udc8a|\ud83d\udc8b|\ud83d\udc8c|\ud83d\udc8d|\ud83d\udc8e|\ud83d\udc8f|\ud83d\udc90|\ud83d\udc91|\ud83d\udc92|\ud83d\udc93|\ud83d\udc94|\ud83d\udc95|\ud83d\udc96|\ud83d\udc97|\ud83d\udc98|\ud83d\udc99|\ud83d\udc9a|\ud83d\udc9b|\ud83d\udc9c|\ud83d\udc9d|\ud83d\udc9e|\ud83d\udc9f|\ud83d\udca0|\ud83d\udca1|\ud83d\udca2|\ud83d\udca3|\ud83d\udca4|\ud83d\udca5|\ud83d\udca6|\ud83d\udca7|\ud83d\udca8|\ud83d\udca9|\ud83d\udcaa|\ud83d\udcab|\ud83d\udcac|\ud83d\udcae|\ud83d\udcaf|\ud83d\udcb0|\ud83d\udcb1|\ud83d\udcb2|\ud83d\udcb3|\ud83d\udcb4|\ud83d\udcb5|\ud83d\udcb8|\ud83d\udcb9|\ud83d\udcba|\ud83d\udcbb|\ud83d\udcbc|\ud83d\udcbd|\ud83d\udcbe|\ud83d\udcbf|\ud83d\udcc0|\ud83d\udcc1|\ud83d\udcc2|\ud83d\udcc3|\ud83d\udcc4|\ud83d\udcc5|\ud83d\udcc6|\ud83d\udcc7|\ud83d\udcc8|\ud83d\udcc9|\ud83d\udcca|\ud83d\udccb|\ud83d\udccc|\ud83d\udccd|\ud83d\udcce|\ud83d\udccf|\ud83d\udcd0|\ud83d\udcd1|\ud83d\udcd2|\ud83d\udcd3|\ud83d\udcd4|\ud83d\udcd5|\ud83d\udcd6|\ud83d\udcd7|\ud83d\udcd8|\ud83d\udcd9|\ud83d\udcda|\ud83d\udcdb|\ud83d\udcdc|\ud83d\udcdd|\ud83d\udcde|\ud83d\udcdf|\ud83d\udce0|\ud83d\udce1|\ud83d\udce2|\ud83d\udce3|\ud83d\udce4|\ud83d\udce5|\ud83d\udce6|\ud83d\udce7|\ud83d\udce8|\ud83d\udce9|\ud83d\udcea|\ud83d\udceb|\ud83d\udcee|\ud83d\udcf0|\ud83d\udcf1|\ud83d\udcf2|\ud83d\udcf3|\ud83d\udcf4|\ud83d\udcf6|\ud83d\udcf7|\ud83d\udcf9|\ud83d\udcfa|\ud83d\udcfb|\ud83d\udcfc|\ud83d\udd03|\ud83d\udd0a|\ud83d\udd0b|\ud83d\udd0c|\ud83d\udd0d|\ud83d\udd0e|\ud83d\udd0f|\ud83d\udd10|\ud83d\udd11|\ud83d\udd12|\ud83d\udd13|\ud83d\udd14|\ud83d\udd16|\ud83d\udd17|\ud83d\udd18|\ud83d\udd19|\ud83d\udd1a|\ud83d\udd1b|\ud83d\udd1c|\ud83d\udd1d|\ud83d\udd1e|\ud83d\udd1f|\ud83d\udd20|\ud83d\udd21|\ud83d\udd22|\ud83d\udd23|\ud83d\udd24|\ud83d\udd25|\ud83d\udd26|\ud83d\udd27|\ud83d\udd28|\ud83d\udd29|\ud83d\udd2a|\ud83d\udd2b|\ud83d\udd2e|\ud83d\udd2f|\ud83d\udd30|\ud83d\udd31|\ud83d\udd32|\ud83d\udd33|\ud83d\udd34|\ud83d\udd35|\ud83d\udd36|\ud83d\udd37|\ud83d\udd38|\ud83d\udd39|\ud83d\udd3a|\ud83d\udd3b|\ud83d\udd3c|\ud83d\udd3d|\ud83d\udd50|\ud83d\udd51|\ud83d\udd52|\ud83d\udd53|\ud83d\udd54|\ud83d\udd55|\ud83d\udd56|\ud83d\udd57|\ud83d\udd58|\ud83d\udd59|\ud83d\udd5a|\ud83d\udd5b|\ud83d\uddfb|\ud83d\uddfc|\ud83d\uddfd|\ud83d\uddfe|\ud83d\uddff|\ud83d\ude01|\ud83d\ude02|\ud83d\ude03|\ud83d\ude04|\ud83d\ude05|\ud83d\ude06|\ud83d\ude09|\ud83d\ude0a|\ud83d\ude0b|\ud83d\ude0c|\ud83d\ude0d|\ud83d\ude0f|\ud83d\ude12|\ud83d\ude13|\ud83d\ude14|\ud83d\ude16|\ud83d\ude18|\ud83d\ude1a|\ud83d\ude1c|\ud83d\ude1d|\ud83d\ude1e|\ud83d\ude20|\ud83d\ude21|\ud83d\ude22|\ud83d\ude23|\ud83d\ude24|\ud83d\ude25|\ud83d\ude28|\ud83d\ude29|\ud83d\ude2a|\ud83d\ude2b|\ud83d\ude2d|\ud83d\ude30|\ud83d\ude31|\ud83d\ude32|\ud83d\ude33|\ud83d\ude35|\ud83d\ude37|\ud83d\ude38|\ud83d\ude39|\ud83d\ude3a|\ud83d\ude3b|\ud83d\ude3c|\ud83d\ude3d|\ud83d\ude3e|\ud83d\ude3f|\ud83d\ude40|\ud83d\ude45|\ud83d\ude46|\ud83d\ude47|\ud83d\ude48|\ud83d\ude49|\ud83d\ude4a|\ud83d\ude4b|\ud83d\ude4c|\ud83d\ude4d|\ud83d\ude4e|\ud83d\ude4f|\ud83d\ude80|\ud83d\ude83|\ud83d\ude84|\ud83d\ude85|\ud83d\ude87|\ud83d\ude89|\ud83d\ude8c|\ud83d\ude8f|\ud83d\ude91|\ud83d\ude92|\ud83d\ude93|\ud83d\ude95|\ud83d\ude97|\ud83d\ude99|\ud83d\ude9a|\ud83d\udea2|\ud83d\udea4|\ud83d\udea5|\ud83d\udea7|\ud83d\udea8|\ud83d\udea9|\ud83d\udeaa|\ud83d\udeab|\ud83d\udeac|\ud83d\udead|\ud83d\udeb2|\ud83d\udeb6|\ud83d\udeb9|\ud83d\udeba|\ud83d\udebb|\ud83d\udebc|\ud83d\udebd|\ud83d\udebe|\ud83d\udec0|\ud83c\udde6|\ud83c\udde7|\ud83c\udde8|\ud83c\udde9|\ud83c\uddea|\ud83c\uddeb|\ud83c\uddec|\ud83c\udded|\ud83c\uddee|\ud83c\uddef|\ud83c\uddf0|\ud83c\uddf1|\ud83c\uddf2|\ud83c\uddf3|\ud83c\uddf4|\ud83c\uddf5|\ud83c\uddf6|\ud83c\uddf7|\ud83c\uddf8|\ud83c\uddf9|\ud83c\uddfa|\ud83c\uddfb|\ud83c\uddfc|\ud83c\uddfd|\ud83c\uddfe|\ud83c\uddff|\ud83c\udf0d|\ud83c\udf0e|\ud83c\udf10|\ud83c\udf12|\ud83c\udf16|\ud83c\udf17|\ue50a|\u27b0|\u2797|\u2796|\u2795|\u2755|\u2754|\u2753|\u274e|\u274c|\u2728|\u270b|\u270a|\u2705|\u26ce|\u23f3|\u23f0|\u23ec|\u23eb|\u23ea|\u23e9|\u27bf|\u00a9|\u00ae)|(?:(?:\ud83c\udc04|\ud83c\udd70|\ud83c\udd71|\ud83c\udd7e|\ud83c\udd7f|\ud83c\ude02|\ud83c\ude1a|\ud83c\ude2f|\ud83c\ude37|\u3299|\u303d|\u3030|\u2b55|\u2b50|\u2b1c|\u2b1b|\u2b07|\u2b06|\u2b05|\u2935|\u2934|\u27a1|\u2764|\u2757|\u2747|\u2744|\u2734|\u2733|\u2716|\u2714|\u2712|\u270f|\u270c|\u2709|\u2708|\u2702|\u26fd|\u26fa|\u26f5|\u26f3|\u26f2|\u26ea|\u26d4|\u26c5|\u26c4|\u26be|\u26bd|\u26ab|\u26aa|\u26a1|\u26a0|\u2693|\u267f|\u267b|\u3297|\u2666|\u2665|\u2663|\u2660|\u2653|\u2652|\u2651|\u2650|\u264f|\u264e|\u264d|\u264c|\u264b|\u264a|\u2649|\u2648|\u263a|\u261d|\u2615|\u2614|\u2611|\u260e|\u2601|\u2600|\u25fe|\u25fd|\u25fc|\u25fb|\u25c0|\u25b6|\u25ab|\u25aa|\u24c2|\u231b|\u231a|\u21aa|\u21a9|\u2199|\u2198|\u2197|\u2196|\u2195|\u2194|\u2139|\u2122|\u2049|\u203c|\u2668)([\uFE0E\uFE0F]?)))/g,rescaper=/[&<>'"]/g,shouldntBeParsed=/IFRAME|NOFRAMES|NOSCRIPT|SCRIPT|SELECT|STYLE|TEXTAREA|[a-z]/,fromCharCode=String.fromCharCode;return twemoji;function createText(text){return document.createTextNode(text)}function escapeHTML(s){return s.replace(rescaper,replacer)}function defaultImageSrcGenerator(icon,options){return"".concat(options.base,options.size,"/",icon,options.ext)}function grabAllTextNodes(node,allText){var childNodes=node.childNodes,length=childNodes.length,subnode,nodeType;while(length--){subnode=childNodes[length];nodeType=subnode.nodeType;if(nodeType===3){allText.push(subnode)}else if(nodeType===1&&!shouldntBeParsed.test(subnode.nodeName)){grabAllTextNodes(subnode,allText)}}return allText}function grabTheRightIcon(icon,variant){return toCodePoint(variant==="️"?icon.slice(0,-1):icon.length===3&&icon.charAt(1)==="️"?icon.charAt(0)+icon.charAt(2):icon)}function parseNode(node,options){var allText=grabAllTextNodes(node,[]),length=allText.length,attrib,attrname,modified,fragment,subnode,text,match,i,index,img,alt,icon,variant,src;while(length--){modified=false;fragment=document.createDocumentFragment();subnode=allText[length];text=subnode.nodeValue;i=0;while(match=re.exec(text)){index=match.index;if(index!==i){fragment.appendChild(createText(text.slice(i,index)))}alt=match[0];icon=match[1];variant=match[2];i=index+alt.length;if(variant!=="︎"){src=options.callback(grabTheRightIcon(icon,variant),options,variant);if(src){img=new Image;img.onerror=options.onerror;img.setAttribute("draggable","false");attrib=options.attributes(icon,variant);for(attrname in attrib){if(attrib.hasOwnProperty(attrname)&&attrname.indexOf("on")!==0&&!img.hasAttribute(attrname)){img.setAttribute(attrname,attrib[attrname])}}img.className=options.className;img.alt=alt;img.src=src;modified=true;fragment.appendChild(img)}}if(!img)fragment.appendChild(createText(alt));img=null}if(modified){if(i")}}return ret})}function replacer(m){return escaper[m]}function returnNull(){return null}function toSizeSquaredAsset(value){return typeof value==="number"?value+"x"+value:value}function fromCodePoint(codepoint){var code=typeof codepoint==="string"?parseInt(codepoint,16):codepoint;if(code<65536){return fromCharCode(code)}code-=65536;return fromCharCode(55296+(code>>10),56320+(code&1023))}function parse(what,how){if(!how||typeof how==="function"){how={callback:how}}return(typeof what==="string"?parseString:parseNode)(what,{callback:how.callback||defaultImageSrcGenerator,attributes:typeof how.attributes==="function"?how.attributes:returnNull,base:typeof how.base==="string"?how.base:twemoji.base,ext:how.ext||twemoji.ext,size:how.folder||toSizeSquaredAsset(how.size||twemoji.size),className:how.className||twemoji.className,onerror:how.onerror||twemoji.onerror})}function replace(text,callback){return String(text).replace(re,callback)}function test(text){re.lastIndex=0;var result=re.test(text);re.lastIndex=0;return result}function toCodePoint(unicodeSurrogates,sep){var r=[],c=0,p=0,i=0;while(i 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 34 | 35 |
36 |
37 |
38 |
39 | 40 |
41 |
42 |
43 | 44 |

Nothing to be found here

45 |
46 |
47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /frontend/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudron-io/meemo/0ccde1f7f5104c33dcc80053823fa44183756438/frontend/favicon.png -------------------------------------------------------------------------------- /frontend/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml 48 | 53 | 56 | 58 | 59 | 67 | 71 | 75 | 76 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
32 | 33 |
34 | 35 |
36 | 37 |
38 |
39 |
40 | 43 |
44 |
45 |
46 | 47 |
48 | 49 | 50 |   51 | 52 |   53 | 54 |
Uploading files...{{ uploadProgress }}%
55 |
56 |
57 | 58 |
59 |
60 | #{{ tag.name }} 61 |
62 |
63 |


64 |
65 |
66 |
67 |

Archive

68 |
69 |
70 |
Nothing found
71 | 72 | 73 |
74 |
75 |
76 |
77 |
78 | 79 |
80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | <%- include('templates/navigation-bar.html') %> 96 | <%- include('templates/tag-sidebar.html') %> 97 | <%- include('templates/modal-settings.html') %> 98 | <%- include('templates/modal-browser-extensions.html') %> 99 | <%- include('templates/modal-cheatsheet.html') %> 100 | <%- include('templates/modal-import.html') %> 101 | <%- include('templates/view-login.html') %> 102 | <%- include('templates/view-loading.html') %> 103 | <%- include('templates/thing.html') %> 104 | 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /frontend/js/core.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var g_server = location.origin; 5 | var g_token = localStorage.token || ''; 6 | 7 | function guid() { 8 | function s4() { 9 | return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); 10 | } 11 | 12 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); 13 | } 14 | 15 | function errorWrapper(callback) { 16 | return function (error, result) { 17 | if (error && error.status === 401) return window.Guacamoly.Core.onAuthFailure(); 18 | 19 | callback(error, result); 20 | }; 21 | } 22 | 23 | function url(path) { 24 | return g_server + path; 25 | } 26 | 27 | function origin() { 28 | return g_server; 29 | } 30 | 31 | function token() { 32 | return g_token; 33 | } 34 | 35 | function Thing(id, createdAt, modifiedAt, tags, content, richContent, attachments, isPublic, isArchived, isSticky) { 36 | this.id = id; 37 | this.createdAt = createdAt || 0; 38 | this.modifiedAt = modifiedAt || 0; 39 | this.tags = tags || []; 40 | this.content = content; 41 | this.edit = false; 42 | this.richContent = richContent; 43 | this.attachments = attachments || []; 44 | this.public = !!isPublic; 45 | this.archived = !!isArchived; 46 | this.sticky = !!isSticky; 47 | } 48 | 49 | function ThingsApi() { 50 | this._addCallbacks = []; 51 | this._editCallbacks = []; 52 | this._delCallbacks = []; 53 | this._operation = ''; 54 | this._query = null; 55 | } 56 | 57 | ThingsApi.prototype.get = function (filter, isArchived, callback) { 58 | var that = this; 59 | var u = url('/api/things'); 60 | var operation = guid(); 61 | 62 | this._operation = operation; 63 | 64 | this._query = {}; 65 | 66 | if (filter) this._query.filter = filter; 67 | if (isArchived) this._query.archived = true; 68 | 69 | // TODO add ui to toggle this based on user preference 70 | this._query.sticky = true; 71 | 72 | this._query.skip = 0; 73 | this._query.limit = 10; 74 | 75 | superagent.get(u).query(this._query).end(errorWrapper(function (error, result) { 76 | // ignore this if we moved on 77 | if (that._operation !== operation) { 78 | console.log('ignore this call'); 79 | return; 80 | } 81 | 82 | if (error && !error.response) return callback(error); 83 | if (result.status !== 200) return callback(new Error('Failed: ' + result.status + '. ' + result.text)); 84 | 85 | var tmp = result.body.things.map(function (thing) { 86 | return new Thing(thing._id, new Date(thing.createdAt).getTime(), new Date(thing.modifiedAt).getTime(), thing.tags, thing.content, thing.richContent, thing.attachments, thing.public, thing.archived, thing.sticky); 87 | }); 88 | 89 | // update skip for fetch more call 90 | that._query.skip += result.body.things.length; 91 | 92 | callback(null, tmp); 93 | })); 94 | }; 95 | 96 | ThingsApi.prototype.fetchMore = function (callback) { 97 | var that = this; 98 | var u = url('/api/things'); 99 | 100 | if (!this._query) return callback(new Error('no previous query')); 101 | 102 | superagent.get(u).query(this._query).end(errorWrapper(function (error, result) { 103 | if (error && !error.response) return callback(error); 104 | if (result.status !== 200) return callback(new Error('Failed: ' + result.status + '. ' + result.text)); 105 | 106 | var tmp = result.body.things.map(function (thing) { 107 | return new Thing(thing._id, new Date(thing.createdAt).getTime(), new Date(thing.modifiedAt).getTime(), thing.tags, thing.content, thing.richContent, thing.attachments, thing.public, thing.archived, thing.sticky); 108 | }); 109 | 110 | // update skip for next call 111 | that._query.skip += result.body.things.length; 112 | 113 | callback(null, tmp); 114 | })); 115 | }; 116 | 117 | ThingsApi.prototype.add = function (content, attachments, callback) { 118 | var that = this; 119 | 120 | superagent.post(url('/api/things')).send({ content: content, attachments: attachments }).end(errorWrapper(function (error, result) { 121 | if (error && !error.response) return callback(error); 122 | if (result.status !== 201) return callback(new Error('Failed: ' + result.status + '. ' + result.text)); 123 | 124 | var tmp = result.body.thing; 125 | var thing = new Thing(tmp._id, new Date(tmp.createdAt).getTime(), new Date(tmp.modifiedAt).getTime(), tmp.tags, tmp.content, tmp.richContent, tmp.attachments, tmp.public, tmp.archived, tmp.sticky); 126 | 127 | that._addCallbacks.forEach(function (callback) { 128 | setTimeout(callback.bind(null, thing), 0); 129 | }); 130 | 131 | callback(null, thing); 132 | })); 133 | }; 134 | 135 | ThingsApi.prototype.edit = function (thing, callback) { 136 | var that = this; 137 | 138 | superagent.put(url('/api/things/' + thing.id)).send(thing).end(errorWrapper(function (error, result) { 139 | if (error && !error.response) return callback(error); 140 | if (result.status !== 201) return callback(new Error('Failed: ' + result.status + '. ' + result.text)); 141 | 142 | that._editCallbacks.forEach(function (callback) { 143 | setTimeout(callback.bind(null, thing), 0); 144 | }); 145 | 146 | callback(null, result.body.thing); 147 | })); 148 | }; 149 | 150 | ThingsApi.prototype.del = function (thing, callback) { 151 | var that = this; 152 | 153 | superagent.del(url('/api/things/' + thing.id)).end(errorWrapper(function (error, result) { 154 | if (error && !error.response) return callback(error); 155 | if (result.status !== 200) return callback(new Error('Failed: ' + result.status + '. ' + result.text)); 156 | 157 | that._delCallbacks.forEach(function (callback) { 158 | setTimeout(callback.bind(null, thing), 0); 159 | }); 160 | 161 | callback(null); 162 | })); 163 | }; 164 | 165 | ThingsApi.prototype.getPublicThing = function (userId, thingId, callback) { 166 | superagent.get(url('/api/public/' + userId + '/things/' + thingId)).end(errorWrapper(function (error, result) { 167 | if (error && !error.response) return callback(error); 168 | if (result.status !== 200) return callback(new Error('Failed: ' + result.status + '. ' + result.text)); 169 | 170 | var thing = result.body.thing; 171 | 172 | callback(null, new Thing(thing._id, new Date(thing.createdAt).getTime(), new Date(thing.modifiedAt).getTime(), thing.tags, thing.content, thing.richContent, thing.attachments, thing.public)); 173 | })); 174 | }; 175 | 176 | ThingsApi.prototype.getPublic = function (userId, filter, callback) { 177 | var that = this; 178 | var u = url('/api/public/' + userId + '/things'); 179 | var operation = guid(); 180 | 181 | this._operation = operation; 182 | 183 | this._query = {}; 184 | 185 | if (filter) this._query.filter = filter; 186 | this._query.skip = 0; 187 | this._query.limit = 10; 188 | 189 | superagent.get(u).query(this._query).end(errorWrapper(function (error, result) { 190 | // ignore this if we moved on 191 | if (that._operation !== operation) { 192 | console.log('ignore this call'); 193 | return; 194 | } 195 | 196 | if (error && !error.response) return callback(error); 197 | if (result.status !== 200) return callback(new Error('Failed: ' + result.status + '. ' + result.text)); 198 | 199 | var tmp = result.body.things.map(function (thing) { 200 | return new Thing(thing._id, new Date(thing.createdAt).getTime(), new Date(thing.modifiedAt).getTime(), thing.tags, thing.content, thing.richContent, thing.attachments, thing.public); 201 | }); 202 | 203 | // update skip for fetch more call 204 | that._query.skip += result.body.things.length; 205 | 206 | callback(null, tmp); 207 | })); 208 | }; 209 | 210 | ThingsApi.prototype.fetchMorePublic = function (userId, callback) { 211 | var that = this; 212 | var u = url('/api/public/' + userId + '/things'); 213 | 214 | if (!this._query) return callback(new Error('no previous query')); 215 | 216 | superagent.get(u).query(this._query).end(errorWrapper(function (error, result) { 217 | if (error && !error.response) return callback(error); 218 | if (result.status !== 200) return callback(new Error('Failed: ' + result.status + '. ' + result.text)); 219 | 220 | var tmp = result.body.things.map(function (thing) { 221 | return new Thing(thing._id, new Date(thing.createdAt).getTime(), new Date(thing.modifiedAt).getTime(), thing.tags, thing.content, thing.richContent, thing.attachments, thing.public); 222 | }); 223 | 224 | // update skip for next call 225 | that._query.skip += result.body.things.length; 226 | 227 | callback(null, tmp); 228 | })); 229 | }; 230 | 231 | 232 | ThingsApi.prototype.import = function (formData, callback) { 233 | superagent.post(url('/api/import')).send(formData).end(errorWrapper(function (error, result) { 234 | if (error) return callback(error); 235 | callback(null); 236 | })); 237 | }; 238 | 239 | ThingsApi.prototype.uploadFile = function (formData, progressHandler, callback) { 240 | superagent.post(url('/api/files')).send(formData).on('progress', function (event) { 241 | if (event.loaded && event.total) progressHandler(event.loaded / event.total); 242 | }).end(errorWrapper(function (error, result) { 243 | if (error) return callback(error); 244 | callback(null, result.body); 245 | })); 246 | }; 247 | 248 | ThingsApi.prototype.onAdded = function (callback) { 249 | this._addCallbacks.push(callback); 250 | }; 251 | 252 | ThingsApi.prototype.onEdited = function (callback) { 253 | this._editCallbacks.push(callback); 254 | }; 255 | 256 | ThingsApi.prototype.onDeleted = function (callback) { 257 | this._delCallbacks.push(callback); 258 | }; 259 | 260 | ThingsApi.prototype.export = function () { 261 | window.location.href = url('/api/export'); 262 | }; 263 | 264 | function SettingsApi() { 265 | this._changeCallbacks = []; 266 | } 267 | 268 | SettingsApi.prototype.save = function (data, callback) { 269 | var that = this; 270 | 271 | superagent.post(url('/api/settings')).send({ settings: data }).end(errorWrapper(function (error, result) { 272 | if (error && !error.response) return callback(error); 273 | if (result.status !== 202) return callback(new Error('Failed: ' + result.status + '. ' + result.text)); 274 | 275 | that._changeCallbacks.forEach(function (callback) { 276 | setTimeout(callback.bind(null, data), 0); 277 | }); 278 | 279 | callback(null); 280 | })); 281 | }; 282 | 283 | SettingsApi.prototype.get = function (callback) { 284 | superagent.get(url('/api/settings')).end(errorWrapper(function (error, result) { 285 | if (error && !error.response) return callback(error); 286 | if (result.status !== 200) return callback(new Error('Failed: ' + result.status + '. ' + result.text)); 287 | 288 | // just ensure we have defaults 289 | if (!result.body.settings.title) result.body.settings.title = 'Meemo'; 290 | if (typeof result.body.settings.wide === 'undefined') result.body.settings.wide = false; 291 | if (typeof result.body.settings.wideNavbar === 'undefined') result.body.settings.wideNavbar = true; 292 | if (!result.body.settings.backgroundImageDataUrl) result.body.settings.backgroundImageDataUrl = ''; 293 | if (typeof result.body.settings.keepPositionAfterEdit === 'undefined') result.body.settings.keepPositionAfterEdit = false; 294 | if (typeof result.body.settings.publicBackground === 'undefined') result.body.settings.publicBackground = false; 295 | if (typeof result.body.settings.showTagSidebar === 'undefined') result.body.settings.showTagSidebar = false; 296 | 297 | callback(null, result.body.settings); 298 | })); 299 | }; 300 | 301 | SettingsApi.prototype.onChanged = function (callback) { 302 | this._changeCallbacks.push(callback); 303 | }; 304 | 305 | function TagsApi() {} 306 | 307 | TagsApi.prototype.get = function (callback) { 308 | superagent.get(url('/api/tags')).end(errorWrapper(function (error, result) { 309 | if (error && !error.response) return callback(error); 310 | if (result.status !== 200) return callback(new Error('Failed: ' + result.status + '. ' + result.text)); 311 | 312 | result.body.tags.sort(function (a, b) { return a.name > b.name; }); 313 | 314 | callback(null, result.body.tags); 315 | })); 316 | }; 317 | 318 | function SessionApi() {} 319 | 320 | SessionApi.prototype.login = function (username, password, callback) { 321 | superagent.post(g_server + '/api/login').send({ username: username, password: password }).end(function (error, result) { 322 | if (error && !error.response) return callback(error); 323 | if (result.status !== 201) return callback(error); 324 | 325 | g_token = result.body.token; 326 | localStorage.token = g_token; 327 | 328 | callback(null, result.body.user); 329 | }); 330 | }; 331 | 332 | SessionApi.prototype.logout = function () { 333 | superagent.get(url('/api/logout')).end(function (error, result) { 334 | if (error && !error.response) console.error(error); 335 | if (result.status !== 200) console.error('Logout failed.', result.status, result.text); 336 | 337 | g_token = ''; 338 | delete localStorage.token; 339 | 340 | window.Guacamoly.Core.onLogout(); 341 | }); 342 | }; 343 | 344 | SessionApi.prototype.profile = function (callback) { 345 | superagent.get(url('/api/profile')).end(errorWrapper(function (error, result) { 346 | if (error && !error.response) return callback(error); 347 | if (result.status !== 200) return callback(new Error('Failed: ' + result.status + '. ' + result.text)); 348 | 349 | callback(null, result.body); 350 | })); 351 | }; 352 | 353 | function UsersApi() {} 354 | 355 | UsersApi.prototype.list = function (callback) { 356 | superagent.get(url('/api/users')).end(function (error, result) { 357 | if (error && !error.response) return callback(error); 358 | if (result.status !== 200) return callback(new Error('User listing failed. ' + result.status + '. ' + result.text)); 359 | 360 | callback(null, result.body.users); 361 | }); 362 | }; 363 | 364 | UsersApi.prototype.publicProfile = function (userId, callback) { 365 | superagent.get(url('/api/users/' + userId)).end(function (error, result) { 366 | if (error && !error.response) return callback(error); 367 | if (result.status !== 200) return callback(new Error('Fetching public profile failed. ' + result.status + '. ' + result.text)); 368 | 369 | callback(null, result.body); 370 | }); 371 | }; 372 | 373 | window.Guacamoly = window.Guacamoly || {}; 374 | window.Guacamoly.Core = { 375 | onAuthFailure: function () {}, 376 | onLogout: function () {}, 377 | url: url, 378 | origin: origin, 379 | token: token, 380 | Thing: Thing, 381 | session: new SessionApi(), 382 | settings: new SettingsApi(), 383 | things: new ThingsApi(), 384 | tags: new TagsApi(), 385 | users: new UsersApi() 386 | }; 387 | 388 | })(); 389 | -------------------------------------------------------------------------------- /frontend/js/index.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var Core = window.Guacamoly.Core; 5 | 6 | // poor man's async in the global namespace 7 | window.asyncForEach = function asyncForEach(items, handler, callback) { 8 | var cur = 0; 9 | 10 | if (items.length === 0) return callback(); 11 | 12 | (function iterator() { 13 | handler(items[cur], function (error) { 14 | if (error) return callback(error); 15 | if (cur >= items.length-1) return callback(); 16 | ++cur; 17 | 18 | iterator(); 19 | }); 20 | })(); 21 | }; 22 | 23 | // Tag propasal filter 24 | Vue.getCurrentSearchWord = function (search, inputElement) { 25 | var cursorPos = $(inputElement)[0] ? $(inputElement)[0].selectionStart : -1; 26 | var word = ''; 27 | 28 | if (cursorPos === -1) return ''; 29 | 30 | for (var i = 0; i < search.length; ++i) { 31 | // break if we went beyond and we hit a space 32 | if (i >= cursorPos && (search[i] === ' ' || search[i] === '\n')) break; 33 | 34 | if (search[i] === ' ' || search[i] === '\n') word = ''; 35 | else word += search[i]; 36 | } 37 | 38 | return word; 39 | }; 40 | 41 | function proposeTags(options, search, inputSelector, requireHash, threshold) { 42 | var raw = Vue.getCurrentSearchWord(search, $(inputSelector)); 43 | 44 | if (requireHash && raw[0] !== '#') return []; 45 | 46 | var word = raw.replace(/^#/, ''); 47 | 48 | if (threshold && threshold > word.length) return []; 49 | 50 | return options.filter(function (o) { 51 | return o.name.indexOf(word) >= 0; 52 | }); 53 | } 54 | 55 | Vue.filter('proposeTags', proposeTags); 56 | Vue.filter('proposeTagsThingsEdit', function (options, search, id) { 57 | return proposeTags(options, search, $('#textarea-' + id), true, 1); 58 | }); 59 | 60 | function popularTags(options, amount) { 61 | amount = amount || 15; 62 | 63 | return options.slice().sort(function (a, b) { return b.usage - a.usage; }).slice(0, amount); 64 | } 65 | 66 | Vue.filter('popularTags', popularTags); 67 | 68 | var vue = new Vue({ 69 | el: '#application', 70 | data: { 71 | Core: window.Guacamoly.Core, 72 | tags: [], 73 | things: [], 74 | busyAdd: false, 75 | busyThings: true, 76 | busyFetchMore: false, 77 | search: '', 78 | archived: false, 79 | sticky: false, 80 | profile: {}, 81 | settings: {}, 82 | mainView: '', 83 | thingContent: '', 84 | thingAttachments: [], 85 | uploadProgress: -1 86 | }, 87 | methods: { 88 | giveAddFocus: function () { 89 | $('#addTextarea').focus(); 90 | }, 91 | addThing: function () { 92 | var that = this; 93 | 94 | this.busyAdd = true; 95 | 96 | Core.things.add(this.thingContent, this.thingAttachments, function (error, thing) { 97 | that.busyAdd = false; 98 | 99 | if (error) return console.error(error); 100 | that.thingContent = ''; 101 | that.thingAttachments = []; 102 | that.things.unshift(thing); 103 | 104 | that.refreshTags(); 105 | }); 106 | }, 107 | refreshTags: function (callback) { 108 | var that = this; 109 | 110 | Core.tags.get(function (error, tags) { 111 | if (error) return console.error(error); 112 | 113 | that.tags = tags; 114 | 115 | if (callback) callback(); 116 | }); 117 | }, 118 | refresh: function (search) { 119 | var that = this; 120 | 121 | this.busyThings = true; 122 | 123 | window.location.href = '/#search?' + (search ? encodeURIComponent(search) : ''); 124 | 125 | Core.things.get(search || '', this.archived, function (error, data) { 126 | if (error) return console.error(error); 127 | 128 | that.things = data; 129 | that.busyThings = false; 130 | }); 131 | }, 132 | triggerAttachmentUpload: function () { 133 | $('#addAttachment').click(); 134 | }, 135 | attachmentChanged: function (event) { 136 | var that = this; 137 | 138 | var count = event.target.files.length; 139 | var stepSize = 100 / count; 140 | var currentIndex = 0; 141 | 142 | this.uploadProgress = 1; 143 | 144 | asyncForEach(event.target.files, function (file, callback) { 145 | var formData = new FormData(); 146 | formData.append('file', file); 147 | 148 | that.$root.Core.things.uploadFile(formData, function (progress) { 149 | var tmp = Math.ceil(stepSize * currentIndex + (stepSize * progress)); 150 | that.uploadProgress = tmp > 100 ? 100 : tmp; 151 | }, function (error, result) { 152 | if (error) return callback(error); 153 | 154 | currentIndex++; 155 | 156 | that.thingContent += ' [' + result.fileName + '] '; 157 | that.thingAttachments.push(result); 158 | 159 | callback(); 160 | }); 161 | }, function (error) { 162 | if (error) console.error('Error uploading file.', error); 163 | 164 | that.uploadProgress = -1; 165 | }); 166 | }, 167 | activateProposedTag: function (tag) { 168 | var word = Vue.getCurrentSearchWord(this.thingContent, $('#addTextarea')); 169 | if (!word) console.log('nothing to add'); 170 | 171 | var cursorPosition = $('#addTextarea')[0].selectionStart; 172 | 173 | this.thingContent = this.thingContent.replace(new RegExp(word, 'g'), function (match, offset) { 174 | return ((cursorPosition - word.length) === offset) ? ('#' + tag.name) : match; 175 | }); 176 | 177 | Vue.nextTick(function () { $('#addTextarea').focus(); }); 178 | }, 179 | // prevent from bubbling up to the main drop handler to allow textarea drops and paste 180 | preventEventBubble: function (event) { 181 | event.cancelBubble = true; 182 | }, 183 | dragOver: function (event) { 184 | event.preventDefault(); 185 | }, 186 | dropOrPasteHandler: dropOrPasteHandler, 187 | main: function () { 188 | var that = this; 189 | 190 | this.mainView = 'loader'; 191 | 192 | Core.session.profile(function (error, profile) { 193 | if (error) return console.error(error); 194 | 195 | that.profile = profile.user; 196 | 197 | Core.settings.get(function (error, settings) { 198 | if (error) return console.error(error); 199 | 200 | // set initial settings 201 | that.settings = settings; 202 | 203 | if (settings.title) window.document.title = settings.title; 204 | if (settings.backgroundImageDataUrl) window.document.body.style.backgroundImage = 'url("' + settings.backgroundImageDataUrl + '")'; 205 | 206 | that.refreshTags(function () { 207 | that.mainView = 'content'; 208 | 209 | window.setTimeout(function () { $('#searchBarInput').focus(); }, 0); 210 | 211 | hashChangeHandler(); 212 | 213 | // add global object for browser extensions 214 | document.getElementById('guacamoly-settings-node').textContent = JSON.stringify({ 215 | origin: Core.origin(), 216 | token: Core.token(), 217 | title: settings.title 218 | }); 219 | }); 220 | }); 221 | }); 222 | } 223 | }, 224 | ready: function () { 225 | // Register event handlers 226 | shortcut.add('Ctrl+s', this.addThing.bind(this), { target: 'addTextarea' }); 227 | shortcut.add('Ctrl+Enter', this.addThing.bind(this), { target: 'addTextarea' }); 228 | shortcut.add('Ctrl+f', function () { $('#searchBarInput').focus(); }, {}); 229 | 230 | this.main(); 231 | } 232 | }); 233 | 234 | function hashChangeHandler() { 235 | var action = window.location.hash.split('?')[0]; 236 | var params = window.location.hash.indexOf('?') > 0 ? decodeURIComponent(window.location.hash.slice(window.location.hash.indexOf('?') + 1)) : null; 237 | 238 | if (action === '#search') { 239 | if (params !== null) vue.search = params; 240 | vue.refresh(vue.search); 241 | } else { 242 | window.location.href = '/#search?'; 243 | } 244 | } 245 | 246 | function scrollHandler() { 247 | // add 1 full pixel to be on the safe side for zoom settings, where pixel values might be floats 248 | if ($(window).height() + $(window).scrollTop() + 1 >= $(document).height()) { 249 | // prevent from refetching while in progress 250 | if (vue.busyFetchMore) return; 251 | 252 | vue.busyFetchMore = true; 253 | 254 | Core.things.fetchMore(function (error, result) { 255 | vue.busyFetchMore = false; 256 | 257 | if (error) return console.error(error); 258 | 259 | vue.things = vue.things.concat(result); 260 | }); 261 | } 262 | } 263 | 264 | function dropOrPasteHandler(event) { 265 | event.cancelBubble = false; 266 | 267 | var data; 268 | if (event.type === 'paste') data = event.clipboardData.items; 269 | else if (event.type === 'drop') data = event.dataTransfer.items; 270 | else return; 271 | 272 | for (var i = 0; i < data.length; ++i) { 273 | if (data[i].kind === 'string') { 274 | // stop if we got a string on a textarea native handling is better 275 | if (event.target.localName === 'textarea') continue; 276 | 277 | if (data[i].type.match('^text/plain')) { 278 | data[i].getAsString(function (s) { 279 | vue.thingContent = vue.thingContent + s; 280 | }); 281 | 282 | event.cancelBubble = true; 283 | event.preventDefault(); 284 | } else { 285 | console.log('Drop type', data[i].type, 'not supported.'); 286 | } 287 | } else if (data[i].kind === 'file') { 288 | var formData = new FormData(); 289 | var file = data[i].getAsFile(); 290 | 291 | // find unused filename 292 | var j = 0; 293 | var name = file.name; 294 | while (vue.thingContent.indexOf(name) !== -1) { 295 | name = j + '_' + file.name; 296 | ++j; 297 | } 298 | formData.append('file', file, name); 299 | 300 | Core.things.uploadFile(formData, function () {}, function (error, result) { 301 | if (error) console.error(error); 302 | 303 | vue.thingContent += ' [' + result.fileName + '] '; 304 | vue.thingAttachments.push(result); 305 | }); 306 | 307 | event.cancelBubble = true; 308 | event.preventDefault(); 309 | } else { 310 | console.error('Unknown drop type', data[i].kind, data[i].type); 311 | } 312 | } 313 | } 314 | 315 | function reset() { 316 | vue.mainView = 'login'; 317 | vue.things = []; 318 | vue.tags = []; 319 | vue.search = ''; 320 | vue.settings = {}; 321 | 322 | window.document.title = 'Meemo'; 323 | window.document.body.style.backgroundImage = ''; 324 | } 325 | 326 | Core.onAuthFailure = reset; 327 | Core.onLogout = reset; 328 | 329 | Core.settings.onChanged(function (data) { 330 | vue.settings.title = data.title || 'Meemo'; 331 | vue.settings.backgroundImageDataUrl = data.backgroundImageDataUrl; 332 | vue.settings.wide = data.wide; 333 | vue.settings.wideNavbar = data.wideNavbar; 334 | vue.settings.keepPositionAfterEdit = data.keepPositionAfterEdit; 335 | vue.settings.publicBackground = data.publicBackground; 336 | vue.settings.showTagSidebar = data.showTagSidebar; 337 | 338 | window.document.title = data.title; 339 | 340 | if (data.backgroundImageDataUrl) window.document.body.style.backgroundImage = 'url("' + data.backgroundImageDataUrl + '")'; 341 | else window.document.body.style.backgroundImage = ''; 342 | }); 343 | 344 | Core.things.onDeleted(function (thing) { 345 | // remove if found 346 | for (var i = 0; i < vue.things.length; ++i) { 347 | if (vue.things[i].id === thing.id) { 348 | vue.things.splice(i, 1); 349 | return; 350 | } 351 | } 352 | }); 353 | 354 | Core.things.onEdited(function (thing) { 355 | if ((thing.archived && !vue.archived) || (!thing.archived && vue.archived)) { 356 | // remove if found 357 | for (var i = 0; i < vue.things.length; ++i) { 358 | if (vue.things[i].id === thing.id) { 359 | vue.things.splice(i, 1); 360 | return; 361 | } 362 | } 363 | } else { 364 | // move to first spot 365 | vue.things.splice(0, 0, vue.things.splice(vue.things.indexOf(thing), 1)[0]); 366 | } 367 | }); 368 | 369 | window.addEventListener('hashchange', hashChangeHandler, false); 370 | window.addEventListener('scroll', scrollHandler, false); 371 | 372 | })(); 373 | -------------------------------------------------------------------------------- /frontend/js/shared.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var Core = window.Guacamoly.Core; 5 | 6 | var vue = new Vue({ 7 | el: '#application', 8 | data: { 9 | busy: true, 10 | error: null, 11 | thing: {}, 12 | publicProfile: {} 13 | }, 14 | methods: { 15 | giveAddFocus: function () { 16 | this.$els.addinput.focus(); 17 | } 18 | } 19 | }); 20 | 21 | function main() { 22 | var search = window.location.search.slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {}); 23 | 24 | if (!search.userId) { 25 | vue.error = 'No userId provided'; 26 | vue.busy = false; 27 | return; 28 | } 29 | 30 | if (!search.id) { 31 | vue.error = 'No id provided'; 32 | vue.busy = false; 33 | return; 34 | } 35 | 36 | Core.users.publicProfile(search.userId, function (error, result) { 37 | if (error) console.error(error); 38 | 39 | vue.publicProfile = result; 40 | 41 | if (result.title) window.document.title = result.title; 42 | if (result.backgroundImageDataUrl) window.document.body.style.backgroundImage = 'url("' + result.backgroundImageDataUrl + '")'; 43 | 44 | Core.things.getPublicThing(search.userId, search.id, function (error, result) { 45 | vue.busy = false; 46 | 47 | if (error) { 48 | console.log(error); 49 | vue.error = 'Not found'; 50 | return; 51 | } 52 | 53 | vue.thing = result; 54 | 55 | window.Guacamoly.disableCheckboxes(); 56 | }); 57 | }); 58 | } 59 | 60 | // Main 61 | main(); 62 | 63 | })(); 64 | -------------------------------------------------------------------------------- /frontend/js/stream.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var Core = window.Guacamoly.Core; 5 | 6 | var vue = new Vue({ 7 | el: '#application', 8 | data: { 9 | busy: true, 10 | busyFetchMore: false, 11 | error: null, 12 | things: [], 13 | publicProfile: {} 14 | }, 15 | methods: { 16 | giveAddFocus: function () { 17 | this.$els.addinput.focus(); 18 | } 19 | } 20 | }); 21 | 22 | var search = window.location.search.slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {}); 23 | var userId; 24 | 25 | function main() { 26 | // support both streams.html?userId= AND /public/ 27 | userId = search.userId || location.pathname.slice('/public/'.length); 28 | 29 | if (!userId) { 30 | vue.error = 'No userId provided'; 31 | vue.busy = false; 32 | return; 33 | } 34 | 35 | // add rss link tag 36 | $('head').append(''); 37 | 38 | Core.users.publicProfile(userId, function (error, result) { 39 | if (error) { 40 | vue.error = 'Not found'; 41 | vue.busy = false; 42 | return; 43 | } 44 | 45 | vue.publicProfile = result; 46 | 47 | if (result.title) window.document.title = result.title; 48 | if (result.backgroundImageDataUrl) window.document.body.style.backgroundImage = 'url("' + result.backgroundImageDataUrl + '")'; 49 | 50 | Core.things.getPublic(userId, '', function (error, result) { 51 | vue.busy = false; 52 | 53 | if (error) { 54 | vue.error = 'Not found'; 55 | return; 56 | } 57 | 58 | vue.things = result; 59 | 60 | window.Guacamoly.disableCheckboxes(); 61 | }); 62 | }); 63 | } 64 | 65 | function scrollHandler() { 66 | if (!userId) return; 67 | 68 | // add 1 full pixel to be on the safe side for zoom settings, where pixel values might be floats 69 | if ($(window).height() + $(window).scrollTop() + 1 >= $(document).height()) { 70 | // prevent from refetching while in progress 71 | if (vue.busyFetchMore) return; 72 | 73 | vue.busyFetchMore = true; 74 | 75 | Core.things.fetchMorePublic(userId, function (error, result) { 76 | vue.busyFetchMore = false; 77 | 78 | if (error) return console.error(error); 79 | 80 | vue.things = vue.things.concat(result); 81 | 82 | window.Guacamoly.disableCheckboxes(); 83 | }); 84 | } 85 | } 86 | 87 | // Main 88 | main(); 89 | 90 | window.addEventListener('scroll', scrollHandler, false); 91 | 92 | })(); 93 | -------------------------------------------------------------------------------- /frontend/js/util.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | Vue.config.debug = true; 5 | 6 | window.Guacamoly = window.Guacamoly || {}; 7 | 8 | window.Guacamoly.disableCheckboxes = function () { 9 | // disable interactive checkboxes 10 | Vue.nextTick(function () { 11 | $('.card-content input[type="checkbox"]').on('click', function (event) { 12 | event.preventDefault(); 13 | }); 14 | }); 15 | }; 16 | 17 | function markdownitCheckbox (md) { 18 | var arrayReplaceAt = md.utils.arrayReplaceAt; 19 | var lastId = 0; 20 | var pattern = /(.*)\[(X|\s|\_|\-)\]\s(.*)/i; 21 | 22 | function splitTextToken (original, Token) { 23 | var checked, id, prelabel, label, matches, nodes, ref, text, token, value; 24 | text = original.content; 25 | nodes = []; 26 | matches = text.match(pattern); 27 | prelabel = matches[1]; 28 | value = matches[2]; 29 | label = matches[3]; 30 | checked = (ref = value === 'X' || value === 'x') !== null ? ref : { 'true' : false }; 31 | 32 | /** 33 | * content pre-checkbox 34 | */ 35 | token = new Token('text', '', 0); 36 | token.content = prelabel; 37 | nodes.push(token); 38 | 39 | /** 40 | * 41 | */ 42 | id = 'checkbox' + lastId; 43 | lastId += 1; 44 | token = new Token('checkbox_input', 'input', 0); 45 | token.attrs = [['type', 'checkbox'], ['id', id]]; 46 | if (checked === true) { 47 | token.attrs.push(['checked', 'true']); 48 | } 49 | nodes.push(token); 50 | 51 | /** 52 | *