nodebb-plugin-composer-default 10.0.16 → 10.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/library.js CHANGED
@@ -3,7 +3,6 @@
3
3
  const url = require('url');
4
4
 
5
5
  const nconf = require.main.require('nconf');
6
- const winston = require.main.require('winston');
7
6
  const validator = require('validator');
8
7
 
9
8
  const plugins = require.main.require('./src/plugins');
@@ -14,6 +13,7 @@ const user = require.main.require('./src/user');
14
13
  const meta = require.main.require('./src/meta');
15
14
  const privileges = require.main.require('./src/privileges');
16
15
  const translator = require.main.require('./src/translator');
16
+ const utils = require.main.require('./src/utils');
17
17
  const helpers = require.main.require('./src/controllers/helpers');
18
18
  const SocketPlugins = require.main.require('./src/socket.io/plugins');
19
19
  const socketMethods = require('./websockets');
@@ -164,7 +164,7 @@ plugin.filterComposerBuild = async function (hookData) {
164
164
 
165
165
  const isEditing = !!req.query.pid;
166
166
  const isGuestPost = postData && parseInt(postData.uid, 10) === 0;
167
- const save_id = generateSaveId(req);
167
+ const save_id = utils.generateSaveId(req.uid);
168
168
  const discardRoute = generateDiscardRoute(req, topicData);
169
169
  const body = await generateBody(req, postData);
170
170
 
@@ -251,16 +251,6 @@ async function generateBody(req, postData) {
251
251
  return postData ? postData.content : '';
252
252
  }
253
253
 
254
- function generateSaveId(req) {
255
- if (req.query.cid) {
256
- return ['composer', req.uid, 'cid', req.query.cid].join(':');
257
- } else if (req.query.tid) {
258
- return ['composer', req.uid, 'tid', req.query.tid].join(':');
259
- } else if (req.query.pid) {
260
- return ['composer', req.uid, 'pid', req.query.pid].join(':');
261
- }
262
- }
263
-
264
254
  async function getPostData(req) {
265
255
  if (!req.query.pid && !req.query.toPid) {
266
256
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-composer-default",
3
- "version": "10.0.16",
3
+ "version": "10.0.18",
4
4
  "description": "Default composer for NodeBB",
5
5
  "main": "library.js",
6
6
  "repository": {
@@ -1,41 +1,34 @@
1
1
  'use strict';
2
2
 
3
3
  define('composer/drafts', ['api', 'alerts'], function (api, alerts) {
4
- var drafts = {};
5
- var saveThrottleId;
4
+ const drafts = {};
6
5
 
7
6
  drafts.init = function (postContainer, postData) {
8
- var draftIconEl = postContainer.find('.draft-icon');
9
- function saveThrottle() {
10
- resetTimeout();
11
-
12
- saveThrottleId = setTimeout(function () {
13
- saveDraft(postContainer, draftIconEl, postData);
14
- }, 1000);
7
+ const draftIconEl = postContainer.find('.draft-icon');
8
+ function doSaveDraft() {
9
+ if (!postData.save_id) {
10
+ postData.save_id = utils.generateSaveId(app.user.uid);
11
+ }
12
+ // Post is modified, save to list of opened drafts
13
+ drafts.addToDraftList('available', postData.save_id);
14
+ drafts.addToDraftList('open', postData.save_id);
15
+ saveDraft(postContainer, draftIconEl, postData);
15
16
  }
16
17
 
17
- postContainer.on('keyup', 'textarea, input.handle, input.title', saveThrottle);
18
- postContainer.on('click', 'input[type="checkbox"]', saveThrottle);
19
- postContainer.on('thumb.uploaded', saveThrottle);
18
+ postContainer.on('keyup', 'textarea, input.handle, input.title', utils.debounce(doSaveDraft, 1000));
19
+ postContainer.on('click', 'input[type="checkbox"]', utils.debounce(doSaveDraft, 1000));
20
+ postContainer.on('click', '[component="category/list"] [data-cid]', utils.debounce(doSaveDraft, 1000));
21
+ postContainer.on('thumb.uploaded', doSaveDraft);
20
22
 
21
23
  draftIconEl.on('animationend', function () {
22
24
  $(this).toggleClass('active', false);
23
25
  });
24
26
 
25
27
  $(window).on('unload', function () {
26
- // Update visibility on all open composers
27
- var open = [];
28
- try {
29
- open = localStorage.getItem('drafts:open');
30
- open = JSON.parse(open) || [];
31
- } catch (e) {
32
- console.warn('[composer/drafts] Could not read list of open/available drafts');
33
- open = [];
34
- }
28
+ // remove all drafts from the open list
29
+ const open = drafts.getList('open');
35
30
  if (open.length) {
36
- open.forEach(function (save_id) {
37
- drafts.updateVisibility('open', save_id);
38
- });
31
+ open.forEach(save_id => drafts.removeFromDraftList('open', save_id));
39
32
  }
40
33
  });
41
34
 
@@ -43,69 +36,75 @@ define('composer/drafts', ['api', 'alerts'], function (api, alerts) {
43
36
  drafts.migrateThumbs(...arguments);
44
37
  };
45
38
 
46
- function resetTimeout() {
47
- if (saveThrottleId) {
48
- clearTimeout(saveThrottleId);
49
- saveThrottleId = 0;
50
- }
51
- }
52
-
53
39
  function getStorage(uid) {
54
40
  return parseInt(uid, 10) > 0 ? localStorage : sessionStorage;
55
41
  }
56
42
 
57
43
  drafts.get = function (save_id) {
58
- var uid = save_id.split(':')[1];
59
- var storage = getStorage(uid);
60
- var draft = {
61
- text: storage.getItem(save_id),
62
- };
63
- ['cid', 'title', 'tags', 'uuid'].forEach(function (key) {
64
- const value = storage.getItem(save_id + ':' + key);
65
- if (value) {
66
- draft[key] = value;
44
+ if (!save_id) {
45
+ return null;
46
+ }
47
+ const uid = save_id.split(':')[1];
48
+ const storage = getStorage(uid);
49
+ try {
50
+ const draftJson = storage.getItem(save_id);
51
+ const draft = JSON.parse(draftJson) || null;
52
+ if (!draft) {
53
+ throw new Error(`can't parse draft json for ${save_id}`);
67
54
  }
68
- });
69
- if (!parseInt(uid, 10)) {
70
- draft.handle = storage.getItem(save_id + ':handle');
55
+ draft.save_id = save_id;
56
+ if (draft.timestamp) {
57
+ draft.timestampISO = utils.toISOString(draft.timestamp);
58
+ }
59
+ $(window).trigger('action:composer.drafts.get', {
60
+ save_id: save_id,
61
+ draft: draft,
62
+ storage: storage,
63
+ });
64
+ return draft;
65
+ } catch (e) {
66
+ console.warn(`[composer/drafts] Could not get draft ${save_id}, removing`);
67
+ drafts.removeFromDraftList('available');
68
+ drafts.removeFromDraftList('open');
69
+ return null;
71
70
  }
72
-
73
- $(window).trigger('action:composer.drafts.get', {
74
- save_id: save_id,
75
- draft: draft,
76
- storage: storage,
77
- });
78
- return draft;
79
71
  };
80
72
 
81
73
  function saveDraft(postContainer, draftIconEl, postData) {
82
74
  if (canSave(app.user.uid ? 'localStorage' : 'sessionStorage') && postData && postData.save_id && postContainer.length) {
83
75
  const titleEl = postContainer.find('input.title');
84
76
  const title = titleEl && titleEl.val();
85
- var raw = postContainer.find('textarea').val();
86
- var storage = getStorage(app.user.uid);
87
-
88
- if (postData.hasOwnProperty('cid') && !postData.save_id.endsWith(':cid:' + postData.cid)) {
89
- // A new cid was selected, the save_id needs updating
90
- drafts.removeDraft(postData.save_id); // First, delete the old draft
91
- postData.save_id = postData.save_id.replace(/cid:\d+$/, 'cid:' + postData.cid); // then create a new save_id
92
- }
77
+ const raw = postContainer.find('textarea').val();
78
+ const storage = getStorage(app.user.uid);
93
79
 
94
80
  if (raw.length || (title && title.length)) {
95
- storage.setItem(postData.save_id, raw);
96
- storage.setItem(`${postData.save_id}:uuid`, postContainer.attr('data-uuid'));
97
-
98
- if (postData.hasOwnProperty('cid')) {
81
+ const draftData = {
82
+ action: postData.action,
83
+ text: raw,
84
+ uuid: postContainer.attr('data-uuid'),
85
+ timestamp: Date.now(),
86
+ };
87
+
88
+ if (postData.action === 'topics.post') {
99
89
  // New topic only
100
90
  const tags = postContainer.find('input.tags').val();
101
- storage.setItem(postData.save_id + ':tags', tags);
102
- storage.setItem(postData.save_id + ':title', title);
91
+ draftData.tags = tags;
92
+ draftData.title = title;
93
+ draftData.cid = postData.cid;
94
+ } else if (postData.action === 'posts.reply') {
95
+ // new reply only
96
+ draftData.title = postData.title;
97
+ draftData.tid = postData.tid;
98
+ } else if (postData.action === 'posts.edit') {
99
+ draftData.pid = postData.pid;
103
100
  }
104
101
  if (!app.user.uid) {
105
- var handle = postContainer.find('input.handle').val();
106
- storage.setItem(postData.save_id + ':handle', handle);
102
+ draftData.handle = postContainer.find('input.handle').val();
107
103
  }
108
104
 
105
+ // save all draft data into single item as json
106
+ storage.setItem(postData.save_id, JSON.stringify(draftData));
107
+
109
108
  $(window).trigger('action:composer.drafts.save', {
110
109
  storage: storage,
111
110
  postData: postData,
@@ -122,52 +121,66 @@ define('composer/drafts', ['api', 'alerts'], function (api, alerts) {
122
121
  if (!save_id) {
123
122
  return;
124
123
  }
125
- resetTimeout();
124
+
126
125
  // Remove save_id from list of open and available drafts
127
- drafts.updateVisibility('available', save_id);
128
- drafts.updateVisibility('open', save_id);
129
- var uid = save_id.split(':')[1];
130
- var storage = getStorage(uid);
131
- const keys = Object.keys(storage).filter(key => key.startsWith(save_id));
132
- keys.forEach(key => storage.removeItem(key));
126
+ drafts.removeFromDraftList('available', save_id);
127
+ drafts.removeFromDraftList('open', save_id);
128
+ const uid = save_id.split(':')[1];
129
+ const storage = getStorage(uid);
130
+ storage.removeItem(save_id);
131
+
132
+ $(window).trigger('action:composer.drafts.remove', {
133
+ storage: storage,
134
+ save_id: save_id,
135
+ });
133
136
  };
134
137
 
135
- drafts.updateVisibility = function (set, save_id, add) {
136
- if (!canSave(app.user.uid ? 'localStorage' : 'sessionStorage') || !save_id) {
137
- return;
138
- }
139
- var open = [];
138
+ drafts.getList = function (set) {
140
139
  try {
141
- open = localStorage.getItem('drafts:' + set);
142
- open = open ? JSON.parse(open) : [];
140
+ const draftIds = localStorage.getItem(`drafts:${set}`);
141
+ return JSON.parse(draftIds) || [];
143
142
  } catch (e) {
144
- console.warn('[composer/drafts] Could not read list of open drafts');
145
- open = [];
143
+ console.warn('[composer/drafts] Could not read list of available drafts');
144
+ return [];
146
145
  }
147
- var idx = open.indexOf(save_id);
146
+ };
148
147
 
149
- if (add && idx === -1) {
150
- open.push(save_id);
151
- } else if (!add && idx !== -1) {
152
- open.splice(idx, 1);
153
- } // otherwise do nothing
148
+ drafts.addToDraftList = function (set, save_id) {
149
+ if (!canSave(app.user.uid ? 'localStorage' : 'sessionStorage') || !save_id) {
150
+ return;
151
+ }
152
+ const list = drafts.getList(set);
153
+ if (!list.includes(save_id)) {
154
+ list.push(save_id);
155
+ localStorage.setItem('drafts:' + set, JSON.stringify(list));
156
+ }
157
+ };
154
158
 
155
- localStorage.setItem('drafts:' + set, JSON.stringify(open));
159
+ drafts.removeFromDraftList = function (set, save_id) {
160
+ if (!canSave(app.user.uid ? 'localStorage' : 'sessionStorage') || !save_id) {
161
+ return;
162
+ }
163
+ const list = drafts.getList(set);
164
+ if (list.includes(save_id)) {
165
+ list.splice(list.indexOf(save_id), 1);
166
+ localStorage.setItem('drafts:' + set, JSON.stringify(list));
167
+ }
156
168
  };
157
169
 
158
170
  drafts.migrateGuest = function () {
159
171
  // If any drafts are made while as guest, and user then logs in, assume control of those drafts
160
172
  if (canSave('localStorage') && app.user.uid) {
161
- var test = /^composer:\d+:\w+:\d+(:\w+)?$/;
162
- var keys = Object.keys(sessionStorage).filter(function (key) {
173
+ // composer:<uid>:<timestamp>
174
+ const test = /^composer:\d+:\d$/;
175
+ const keys = Object.keys(sessionStorage).filter(function (key) {
163
176
  return test.test(key);
164
177
  });
165
- var migrated = new Set([]);
166
- var renamed = keys.map(function (key) {
167
- var parts = key.split(':');
178
+ const migrated = new Set([]);
179
+ const renamed = keys.map(function (key) {
180
+ const parts = key.split(':');
168
181
  parts[1] = app.user.uid;
169
182
 
170
- migrated.add(parts.slice(0, 4).join(':'));
183
+ migrated.add(parts.join(':'));
171
184
  return parts.join(':');
172
185
  });
173
186
 
@@ -177,7 +190,7 @@ define('composer/drafts', ['api', 'alerts'], function (api, alerts) {
177
190
  });
178
191
 
179
192
  migrated.forEach(function (save_id) {
180
- drafts.updateVisibility('available', save_id, 1);
193
+ drafts.addToDraftList('available', save_id);
181
194
  });
182
195
 
183
196
  return migrated;
@@ -204,76 +217,77 @@ define('composer/drafts', ['api', 'alerts'], function (api, alerts) {
204
217
  }
205
218
  };
206
219
 
220
+ drafts.listAvailable = function () {
221
+ const available = drafts.getList('available');
222
+ return available.map(drafts.get);
223
+ };
224
+
225
+ drafts.getAvailableCount = function () {
226
+ return drafts.listAvailable().length;
227
+ };
228
+
229
+ drafts.open = function (save_id) {
230
+ if (!save_id) {
231
+ return;
232
+ }
233
+ const draft = drafts.get(save_id);
234
+ openComposer(save_id, draft);
235
+ };
236
+
207
237
  drafts.loadOpen = function () {
208
- if (ajaxify.data.template.login || ajaxify.data.template.register) {
238
+ if (ajaxify.data.template.login || ajaxify.data.template.register || !config.hasOwnProperty('openDraftsOnPageLoad') || !config.openDraftsOnPageLoad) {
209
239
  return;
210
240
  }
211
241
  // Load drafts if they were open
212
- var available;
213
- var open = [];
214
- try {
215
- available = localStorage.getItem('drafts:available');
216
- open = localStorage.getItem('drafts:open');
217
- available = JSON.parse(available) || [];
218
- open = JSON.parse(open) || [];
219
- } catch (e) {
220
- console.warn('[composer/drafts] Could not read list of open/available drafts');
221
- available = [];
222
- open = [];
223
- }
242
+ const available = drafts.getList('available');
243
+ const open = drafts.getList('open');
224
244
 
225
245
  if (available.length) {
226
246
  // Deconstruct each save_id and open up composer
227
247
  available.forEach(function (save_id) {
228
- if (!save_id) {
248
+ if (!save_id || open.includes(save_id)) {
229
249
  return;
230
250
  }
231
- var saveObj = save_id.split(':');
232
- var uid = saveObj[1];
233
- var type = saveObj[2];
234
- var id = saveObj[3];
235
- var draft = drafts.get(save_id);
236
-
237
- // If draft is already open, do nothing
238
- if (open.indexOf(save_id) !== -1) {
251
+ const draft = drafts.get(save_id);
252
+ if (!draft || (!draft.text && !draft.title)) {
253
+ drafts.removeFromDraftList('available', save_id);
254
+ drafts.removeFromDraftList('open', save_id);
239
255
  return;
240
256
  }
241
-
242
- // Don't open other peoples' drafts
243
- if (parseInt(app.user.uid, 10) !== parseInt(uid, 10)) {
244
- return;
245
- }
246
-
247
- if (!draft || (draft.text && draft.title && !draft.text.title && !draft.text.length)) {
248
- // Empty content, remove from list of open drafts
249
- drafts.updateVisibility('available', save_id);
250
- drafts.updateVisibility('open', save_id);
251
- return;
252
- }
253
- require(['composer'], function (composer) {
254
- if (type === 'cid') {
255
- composer.newTopic({
256
- cid: id,
257
- handle: app.user && app.user.uid ? undefined : utils.escapeHTML(draft.handle),
258
- title: utils.escapeHTML(draft.title),
259
- body: utils.escapeHTML(draft.text),
260
- tags: String(draft.tags || '').split(','),
261
- });
262
- } else if (type === 'tid') {
263
- api.get('/topics/' + id, {}, function (err, topicObj) {
264
- if (err) {
265
- return alerts.error(err);
266
- }
267
- composer.newReply(id, undefined, topicObj.title, utils.escapeHTML(draft.text));
268
- });
269
- } else if (type === 'pid') {
270
- composer.editPost(id);
271
- }
272
- });
257
+ openComposer(save_id, draft);
273
258
  });
274
259
  }
275
260
  };
276
261
 
262
+ function openComposer(save_id, draft) {
263
+ const saveObj = save_id.split(':');
264
+ const uid = saveObj[1];
265
+ // Don't open other peoples' drafts
266
+ if (parseInt(app.user.uid, 10) !== parseInt(uid, 10)) {
267
+ return;
268
+ }
269
+ require(['composer'], function (composer) {
270
+ if (draft.action === 'topics.post') {
271
+ composer.newTopic({
272
+ cid: draft.cid,
273
+ handle: app.user && app.user.uid ? undefined : utils.escapeHTML(draft.handle),
274
+ title: utils.escapeHTML(draft.title),
275
+ body: utils.escapeHTML(draft.text),
276
+ tags: String(draft.tags || '').split(','),
277
+ });
278
+ } else if (draft.action === 'posts.reply') {
279
+ api.get('/topics/' + draft.tid, {}, function (err, topicObj) {
280
+ if (err) {
281
+ return alerts.error(err);
282
+ }
283
+ composer.newReply(draft.tid, undefined, topicObj.title, utils.escapeHTML(draft.text));
284
+ });
285
+ } else if (draft.action === 'posts.edit') {
286
+ composer.editPost(draft.pid, draft.text);
287
+ }
288
+ });
289
+ }
290
+
277
291
  // Feature detection courtesy of: https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API
278
292
  function canSave(type) {
279
293
  var storage;
@@ -152,15 +152,6 @@ define('composer', [
152
152
  });
153
153
  });
154
154
 
155
- // Construct a save_id
156
- if (post.hasOwnProperty('cid')) {
157
- post.save_id = ['composer', app.user.uid, 'cid', post.cid].join(':');
158
- } else if (post.hasOwnProperty('tid')) {
159
- post.save_id = ['composer', app.user.uid, 'tid', post.tid].join(':');
160
- } else if (post.hasOwnProperty('pid')) {
161
- post.save_id = ['composer', app.user.uid, 'pid', post.pid].join(':');
162
- }
163
-
164
155
  composer.posts[uuid] = post;
165
156
  composer.load(uuid);
166
157
  }
@@ -273,7 +264,7 @@ define('composer', [
273
264
  });
274
265
  };
275
266
 
276
- composer.editPost = function (pid) {
267
+ composer.editPost = function (pid, text) {
277
268
  socket.emit('plugins.composer.push', pid, function (err, threadData) {
278
269
  if (err) {
279
270
  return alerts.error(err);
@@ -281,6 +272,10 @@ define('composer', [
281
272
  threadData.action = 'posts.edit';
282
273
  threadData.pid = pid;
283
274
  threadData.modified = false;
275
+ if (text) {
276
+ threadData.body = text;
277
+ threadData.modified = true;
278
+ }
284
279
  push(threadData);
285
280
  });
286
281
  };
@@ -334,10 +329,6 @@ define('composer', [
334
329
 
335
330
  postContainer.on('change', 'input, textarea', function () {
336
331
  composer.posts[post_uuid].modified = true;
337
-
338
- // Post is modified, save to list of opened drafts
339
- drafts.updateVisibility('available', composer.posts[post_uuid].save_id, true);
340
- drafts.updateVisibility('open', composer.posts[post_uuid].save_id, true);
341
332
  });
342
333
 
343
334
  submitBtn.on('click', function (e) {
@@ -1,6 +1,5 @@
1
1
  .composer {
2
- @include no-select;
3
-
2
+ user-select: none;
4
3
  background-color: $body-bg;
5
4
  color: $body-color;
6
5
  z-index: $zindex-modal;
@@ -12,6 +12,4 @@
12
12
  </div>
13
13
  </form>
14
14
 
15
- <button id="save" class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored">
16
- <i class="material-icons">save</i>
17
- </button>
15
+ <!-- IMPORT admin/partials/save_button.tpl -->
@@ -1,21 +1,21 @@
1
1
  <div class="title-container">
2
2
  {{{ if isTopic }}}
3
- <div class="category-list-container hidden-sm hidden-xs">
3
+ <div class="category-list-container hidden-sm hidden-xs align-self-center">
4
4
  <!-- IMPORT partials/category-selector.tpl -->
5
5
  </div>
6
6
  {{{ end }}}
7
7
 
8
- <!-- IF showHandleInput -->
8
+ {{{ if showHandleInput }}}
9
9
  <div data-component="composer/handle">
10
10
  <input class="handle form-control h-100" type="text" tabindex="1" placeholder="[[topic:composer.handle_placeholder]]" value="{handle}" />
11
11
  </div>
12
- <!-- ENDIF showHandleInput -->
12
+ {{{ end }}}
13
13
  <div data-component="composer/title" class="position-relative">
14
- <!-- IF isTopicOrMain -->
14
+ {{{ if isTopicOrMain }}}
15
15
  <input class="title form-control h-100" type="text" tabindex="1" placeholder="[[topic:composer.title_placeholder]]" value="{topicTitle}"/>
16
- <!-- ELSE -->
17
- <span class="title h-100">[[topic:composer.replying_to, "{topicTitle}"]]</span>
18
- <!-- ENDIF isTopicOrMain -->
16
+ {{{ else }}}
17
+ <span class="title h-100">{{{ if isEditing }}}[[topic:composer.editing]]{{{ else }}}[[topic:composer.replying_to, "{topicTitle}"]]{{{ end }}}</span>
18
+ {{{ end }}}
19
19
  <div id="quick-search-container" class="quick-search-container mt-2 dropdown-menu d-block p-2 hidden">
20
20
  <div class="text-center loading-indicator"><i class="fa fa-spinner fa-spin"></i></div>
21
21
  <div class="quick-search-results-container"></div>