├── .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 | [](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 |
--------------------------------------------------------------------------------
/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 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | Uploading files...{{ uploadProgress }}%
55 |
56 |
57 |
58 |
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 | *