nodebb-plugin-composer-default 10.3.33 → 11.0.0

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
@@ -1,20 +1,19 @@
1
1
  'use strict';
2
2
 
3
3
 
4
- const nconf = require.main.require('nconf');
5
- const validator = require('validator');
6
-
7
- const plugins = require.main.require('./src/plugins');
8
- const topics = require.main.require('./src/topics');
9
- const categories = require.main.require('./src/categories');
10
- const posts = require.main.require('./src/posts');
11
- const user = require.main.require('./src/user');
12
- const meta = require.main.require('./src/meta');
13
- const privileges = require.main.require('./src/privileges');
14
- const translator = require.main.require('./src/translator');
15
- const utils = require.main.require('./src/utils');
16
- const helpers = require.main.require('./src/controllers/helpers');
17
- const SocketPlugins = require.main.require('./src/socket.io/plugins');
4
+ const nconf = nodebb.require('nconf');
5
+
6
+ const plugins = nodebb.require('./src/plugins');
7
+ const topics = nodebb.require('./src/topics');
8
+ const categories = nodebb.require('./src/categories');
9
+ const posts = nodebb.require('./src/posts');
10
+ const user = nodebb.require('./src/user');
11
+ const meta = nodebb.require('./src/meta');
12
+ const privileges = nodebb.require('./src/privileges');
13
+ const tx = nodebb.require('./src/translator');
14
+ const utils = nodebb.require('./src/utils');
15
+ const helpers = nodebb.require('./src/controllers/helpers');
16
+ const SocketPlugins = nodebb.require('./src/socket.io/plugins');
18
17
  const socketMethods = require('./websockets');
19
18
 
20
19
  const plugin = module.exports;
@@ -23,7 +22,7 @@ plugin.socketMethods = socketMethods;
23
22
 
24
23
  plugin.init = async function (data) {
25
24
  const { router } = data;
26
- const routeHelpers = require.main.require('./src/routes/helpers');
25
+ const routeHelpers = nodebb.require('./src/routes/helpers');
27
26
  const controllers = require('./controllers');
28
27
  SocketPlugins.composer = socketMethods;
29
28
  routeHelpers.setupAdminPageRoute(router, '/admin/plugins/composer-default', controllers.renderAdminPage);
@@ -43,27 +42,20 @@ plugin.addAdminNavigation = async function (header) {
43
42
  return header;
44
43
  };
45
44
 
46
- plugin.getFormattingOptions = async function () {
45
+ plugin.getFormattingOptions = async function (uid) {
47
46
  const defaultVisibility = {
48
- mobile: true,
49
- desktop: true,
47
+ // use d-none d-lg-block, visible on desktop
48
+ // use d-block d-lg-none, visible on mobile
49
+ class: 'd-block',
50
50
 
51
51
  // op or reply
52
52
  main: true,
53
53
  reply: true,
54
54
  };
55
55
  let payload = {
56
+ uid,
56
57
  defaultVisibility,
57
58
  options: [
58
- {
59
- name: 'tags',
60
- title: '[[global:tags.tags]]',
61
- className: 'fa fa-tags',
62
- visibility: {
63
- ...defaultVisibility,
64
- desktop: false,
65
- },
66
- },
67
59
  {
68
60
  name: 'zen',
69
61
  title: '[[modules:composer.zen-mode]]',
@@ -72,16 +64,36 @@ plugin.getFormattingOptions = async function () {
72
64
  },
73
65
  ],
74
66
  };
75
- if (parseInt(meta.config.allowTopicsThumbnail, 10) === 1) {
67
+ const [canUploadImage, canUploadFile] = await privileges.global.can(
68
+ ['upload:post:image', 'upload:post:file'], uid
69
+ );
70
+ if (canUploadImage) {
71
+ if (meta.config.allowTopicsThumbnail) {
72
+ payload.options.push({
73
+ name: 'thumbs',
74
+ title: '[[topic:composer.thumb-title]]',
75
+ className: 'fa fa-address-card-o',
76
+ badge: true,
77
+ visibility: {
78
+ ...defaultVisibility,
79
+ reply: false,
80
+ },
81
+ });
82
+ }
76
83
  payload.options.push({
77
- name: 'thumbs',
78
- title: '[[topic:composer.thumb-title]]',
79
- className: 'fa fa-address-card-o',
80
- badge: true,
81
- visibility: {
82
- ...defaultVisibility,
83
- reply: false,
84
- },
84
+ name: 'picture',
85
+ title: '[[modules:composer.upload-picture]]',
86
+ className: 'fa fa-file-image-o',
87
+ visibility: defaultVisibility,
88
+ });
89
+ }
90
+
91
+ if (canUploadFile) {
92
+ payload.options.push({
93
+ name: 'upload',
94
+ title: '[[modules:composer.upload-file]]',
95
+ className: 'fa fa-file-o',
96
+ visibility: defaultVisibility,
85
97
  });
86
98
  }
87
99
 
@@ -135,7 +147,7 @@ plugin.filterComposerBuild = async function (hookData) {
135
147
  ]),
136
148
  user.isAdministrator(req.uid),
137
149
  isModerator(req),
138
- plugin.getFormattingOptions(),
150
+ plugin.getFormattingOptions(req.uid),
139
151
  getTagWhitelist(req.query, req.uid),
140
152
  privileges.global.get(req.uid),
141
153
  canTag(req),
@@ -161,7 +173,8 @@ plugin.filterComposerBuild = async function (hookData) {
161
173
  const cid = req.query.cid || '';
162
174
  const topicTitle = topicData && topicData.title ?
163
175
  topicData.title :
164
- validator.escape(String(req.query.title || ''));
176
+ String(req.query.title || '');
177
+
165
178
  return {
166
179
  req: req,
167
180
  res: res,
@@ -180,12 +193,6 @@ plugin.filterComposerBuild = async function (hookData) {
180
193
  // can't use title property as that is used for page title
181
194
  topicTitle: topicTitle,
182
195
  titleLength: topicTitle ? topicTitle.length : 0,
183
- titleLabel: translator.compile(
184
- isEditing ?
185
- 'topic:composer.editing-in' :
186
- 'topic:composer.replying-to',
187
- `"${topicTitle}"`
188
- ),
189
196
 
190
197
  topic: topicData,
191
198
  thumb: topicData ? topicData.thumb : '',
@@ -231,7 +238,7 @@ async function checkPrivileges(req, res) {
231
238
 
232
239
  function generateDiscardRoute(req, topicData) {
233
240
  if (req.query.cid) {
234
- return `${nconf.get('relative_path')}/category/${validator.escape(String(req.query.cid))}`;
241
+ return `${nconf.get('relative_path')}/category/${req.query.cid}`;
235
242
  } else if ((req.query.tid || req.query.pid)) {
236
243
  if (topicData) {
237
244
  return `${nconf.get('relative_path')}/topic/${topicData.slug}`;
@@ -245,15 +252,15 @@ async function generateBody(req, postData) {
245
252
  // Quoted reply
246
253
  if (req.query.toPid && parseInt(req.query.quoted, 10) === 1 && postData) {
247
254
  const username = await user.getUserField(postData.uid, 'username');
248
- const translated = await translator.translate(`[[modules:composer.user-said, ${username}]]`);
255
+ const translated = await tx.translateKey('modules:composer.user-said', [tx.escape(username)]);
249
256
  body = `${translated}\n` +
250
257
  `> ${postData ? `${postData.content.replace(/\n/g, '\n> ')}\n\n` : ''}`;
251
258
  } else if (req.query.body || req.query.content) {
252
- body = validator.escape(String(req.query.body || req.query.content));
259
+ body = req.query.body || req.query.content;
253
260
  } else {
254
261
  body = postData ? postData.content : '';
255
262
  }
256
- return translator.escape(body);
263
+ return body;
257
264
  }
258
265
 
259
266
  async function getPostData(req) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-composer-default",
3
- "version": "10.3.33",
3
+ "version": "11.0.0",
4
4
  "description": "Default composer for NodeBB",
5
5
  "main": "library.js",
6
6
  "repository": {
@@ -26,15 +26,13 @@
26
26
  },
27
27
  "readmeFilename": "README.md",
28
28
  "nbbpm": {
29
- "compatibility": "^4.13.0"
29
+ "compatibility": "^4.14.0"
30
30
  },
31
31
  "dependencies": {
32
- "screenfull": "^5.0.2",
33
- "validator": "^13.7.0"
32
+ "screenfull": "^5.0.2"
34
33
  },
35
34
  "devDependencies": {
36
35
  "eslint": "^10.0.0",
37
- "eslint-config-nodebb": "^2.0.1",
38
- "eslint-plugin-import": "^2.31.0"
36
+ "eslint-config-nodebb": "^2.0.2"
39
37
  }
40
38
  }
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  define('composer/categoryList', [
4
- 'categorySelector', 'taskbar', 'api', 'alerts'
4
+ 'categorySelector', 'taskbar', 'api', 'alerts',
5
5
  ], function (categorySelector, taskbar, api, alerts) {
6
6
  const categoryList = {};
7
7
 
@@ -66,14 +66,14 @@ define('composer/drafts', ['api', 'hooks'], function (api, hooks) {
66
66
  }
67
67
  $(window).trigger('action:composer.drafts.get', {
68
68
  save_id: save_id,
69
- draft: draft,
69
+ draft: { ...draft },
70
70
  storage: storage,
71
71
  });
72
72
  return draft;
73
73
  } catch (e) {
74
74
  console.warn(`[composer/drafts] Could not get draft ${save_id}, removing`);
75
- drafts.removeFromDraftList('available');
76
- drafts.removeFromDraftList('open');
75
+ drafts.removeFromDraftList('available', save_id);
76
+ drafts.removeFromDraftList('open', save_id);
77
77
  return null;
78
78
  }
79
79
  };
@@ -263,8 +263,8 @@ define('composer/drafts', ['api', 'hooks'], function (api, hooks) {
263
263
  fromDraft: true,
264
264
  save_id: draft.save_id,
265
265
  cid: draft.cid,
266
- handle: app.user && app.user.uid ? undefined : utils.escapeHTML(draft.handle),
267
- title: utils.escapeHTML(draft.title),
266
+ handle: app.user && app.user.uid ? undefined : draft.handle,
267
+ title: draft.title,
268
268
  body: draft.text,
269
269
  tags: String(draft.tags || '').split(','),
270
270
  thumbs: draft.thumbs || [],
@@ -288,7 +288,7 @@ define('composer/drafts', ['api', 'hooks'], function (api, hooks) {
288
288
  fromDraft: true,
289
289
  save_id: draft.save_id,
290
290
  pid: draft.pid,
291
- title: draft.title ? utils.escapeHTML(draft.title) : undefined,
291
+ title: draft.title || undefined,
292
292
  body: draft.text,
293
293
  thumbs: draft.thumbs || [],
294
294
  };
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
- define('composer/scheduler', ['benchpress', 'bootbox', 'alerts', 'translator'], function (
3
+ define('composer/scheduler', ['benchpress', 'modals', 'alerts', 'translator'], function (
4
4
  Benchpress,
5
- bootbox,
5
+ modals,
6
6
  alerts,
7
7
  translator
8
8
  ) {
@@ -91,7 +91,7 @@ define('composer/scheduler', ['benchpress', 'bootbox', 'alerts', 'translator'],
91
91
 
92
92
  async function openModal() {
93
93
  const html = await Benchpress.render('modals/topic-scheduler');
94
- bootbox.dialog({
94
+ modals.dialog({
95
95
  message: html,
96
96
  title: '[[modules:composer.schedule-for]]',
97
97
  className: 'topic-scheduler',
@@ -20,7 +20,7 @@ define('composer/uploads', [
20
20
 
21
21
  addChangeHandlers(post_uuid);
22
22
 
23
- translator.translate('[[modules:composer.uploading, ' + 0 + '%]]', function (translated) {
23
+ translator.translateKey('modules:composer.uploading', [0]).then((translated) => {
24
24
  uploadingText = translated;
25
25
  });
26
26
  };
@@ -177,7 +177,7 @@ define('composer/uploads', [
177
177
  },
178
178
 
179
179
  uploadProgress: function (event, position, total, percent) {
180
- translator.translate('[[modules:composer.uploading, ' + percent + '%]]', function (translated) {
180
+ translator.translateKey('modules:composer.uploading', [percent]).then(function (translated) {
181
181
  if (doneUploading) {
182
182
  return;
183
183
  }
@@ -16,15 +16,15 @@ define('composer', [
16
16
  'scrollStop',
17
17
  'topicThumbs',
18
18
  'api',
19
- 'bootbox',
19
+ 'modals',
20
20
  'alerts',
21
21
  'hooks',
22
22
  'messages',
23
23
  'search',
24
24
  'screenfull',
25
- ], function (taskbar, translator, uploads, formatting, drafts, tags,
25
+ ], function (taskbar, tx, uploads, formatting, drafts, tags,
26
26
  categoryList, preview, resize, autocomplete, scheduler, postQueue, scrollStop,
27
- topicThumbs, api, bootbox, alerts, hooks, messagesModule, search, screenfull) {
27
+ topicThumbs, api, modals, alerts, hooks, messagesModule, search, screenfull) {
28
28
  const composer = {
29
29
  active: undefined,
30
30
  posts: {},
@@ -52,7 +52,7 @@ define('composer', [
52
52
  return;
53
53
  }
54
54
 
55
- composer.discardConfirm = bootbox.confirm('[[modules:composer.discard]]', function (confirm) {
55
+ composer.discardConfirm = modals.confirm('[[modules:composer.discard]]', function (confirm) {
56
56
  if (confirm) {
57
57
  composer.discard(composer.active);
58
58
  } else {
@@ -106,7 +106,9 @@ define('composer', [
106
106
 
107
107
  // Find a match
108
108
  for (const uuid of Object.keys(composer.posts)) {
109
- if (composer.posts[uuid].hasOwnProperty(type) && id === String(composer.posts[uuid][type])) {
109
+ const openPost = composer.posts[uuid];
110
+ // make sure dom element exists too
111
+ if (openPost.hasOwnProperty(type) && id === String(openPost[type]) && $(`.composer[data-uuid="${uuid}"]`).length) {
110
112
  return uuid;
111
113
  }
112
114
  }
@@ -124,7 +126,7 @@ define('composer', [
124
126
  if (existingUUID) {
125
127
  taskbar.updateActive(existingUUID);
126
128
  if (post.body && !post.fromDraft) {
127
- const postContainer = $('.composer[data-uuid="' + existingUUID + '"]');
129
+ const postContainer = $(`.composer[data-uuid="${existingUUID}"]`);
128
130
  const bodyEl = postContainer.find('textarea');
129
131
  const prevText = bodyEl.val();
130
132
  composer.posts[existingUUID].body = (prevText.length ? prevText + '\n\n' : '') + post.body;
@@ -136,9 +138,9 @@ define('composer', [
136
138
  const uuid = utils.generateUUID();
137
139
  let actionText = '[[topic:composer.new-topic]]';
138
140
  if (post.action === 'posts.reply') {
139
- actionText = translator.compile('topic:composer.replying-to', `"${post.title}"`);
141
+ actionText = tx.compile('topic:composer.replying-to', `"${tx.escape(post.title)}"`);
140
142
  } else if (post.action === 'posts.edit') {
141
- actionText = translator.compile('topic:composer.editing-in', `"${post.title}"`);
143
+ actionText = tx.compile('topic:composer.editing-in', `"${tx.escape(post.title)}"`);
142
144
  }
143
145
 
144
146
  taskbar.push('composer', uuid, {
@@ -203,7 +205,7 @@ define('composer', [
203
205
  push(pushData);
204
206
  };
205
207
 
206
- composer.addQuote = function (data) {
208
+ composer.addQuote = async function (data) {
207
209
  // tid, toPid, selectedPid, title, username, text, uuid
208
210
  data.uuid = data.uuid || composer.active;
209
211
 
@@ -224,16 +226,18 @@ define('composer', [
224
226
  const postHref = `${config.relative_path}/post/${encodeURIComponent(data.selectedPid || data.toPid)}`;
225
227
  const topicLink = `[${escapedTitle}](${postHref})`;
226
228
 
227
- const quoteKey = useTopicLink ?
228
- `> ${translator.compile('modules:composer.user-said-in', data.username, topicLink)}\n>\n` :
229
- `> ${translator.compile('modules:composer.user-said', data.username, postHref)}\n>\n`;
229
+ const txQuoteText = useTopicLink ?
230
+ await tx.translateKey('modules:composer.user-said-in', [tx.escape(data.username), topicLink]) :
231
+ await tx.translateKey('modules:composer.user-said', [tx.escape(data.username), postHref]);
232
+
233
+ const quoteText = `> ${txQuoteText}\n>\n`;
230
234
 
231
235
  if (data.uuid === undefined) {
232
236
  composer.newReply({
233
237
  tid: data.tid,
234
238
  toPid: data.toPid,
235
239
  title: data.title,
236
- body: quoteKey + data.body,
240
+ body: quoteText + data.body,
237
241
  });
238
242
  return;
239
243
  } else if (data.uuid !== composer.active) {
@@ -245,16 +249,13 @@ define('composer', [
245
249
  const bodyEl = postContainer.find('textarea');
246
250
  const prevText = bodyEl.val();
247
251
 
248
- translator.translate(quoteKey, config.defaultLang, function (translated) {
249
- composer.posts[data.uuid].body = (prevText.length ? prevText + '\n\n' : '') + translated + data.body;
250
- bodyEl.val(composer.posts[data.uuid].body);
251
- focusElements(postContainer);
252
- preview.render(postContainer);
253
- });
252
+ composer.posts[data.uuid].body = (prevText.length ? prevText + '\n\n' : '') + quoteText + data.body;
253
+ bodyEl.val(composer.posts[data.uuid].body);
254
+ focusElements(postContainer);
255
+ preview.render(postContainer);
254
256
  };
255
257
 
256
258
  composer.newReply = async function (data) {
257
- const translated = await translator.translate(data.body, config.defaultLang);
258
259
  let pushData = {
259
260
  fromDraft: data.fromDraft,
260
261
  save_id: data.save_id,
@@ -262,8 +263,8 @@ define('composer', [
262
263
  tid: data.tid,
263
264
  toPid: data.toPid,
264
265
  title: data.title,
265
- body: translated,
266
- modified: !!(translated && translated.length),
266
+ body: data.body,
267
+ modified: !!(data.body && data.body.length),
267
268
  isMain: false,
268
269
  };
269
270
  ({ pushData } = await hooks.fire('filter:composer.reply.push', {
@@ -370,7 +371,7 @@ define('composer', [
370
371
  formatting.exitFullscreen();
371
372
 
372
373
  const btn = $(this).prop('disabled', true);
373
- bootbox.confirm('[[modules:composer.discard]]', function (confirm) {
374
+ modals.confirm('[[modules:composer.discard]]', function (confirm) {
374
375
  if (confirm) {
375
376
  composer.discard(post_uuid);
376
377
  removeComposerHistory();
@@ -446,15 +447,10 @@ define('composer', [
446
447
  const topicTemplate = isTopic && postData.category ? postData.category.topicTemplate : '';
447
448
 
448
449
  let data = {
450
+ action: postData.action,
449
451
  topicTitle: postData.title,
450
452
  titleLength: postData.title.length,
451
- titleLabel: translator.compile(
452
- isEditing ?
453
- 'topic:composer.editing-in' :
454
- 'topic:composer.replying-to',
455
- `"${postData.title}"`
456
- ),
457
- body: utils.escapeHTML(translator.escape(postData.body) || topicTemplate),
453
+ body: postData.body || topicTemplate,
458
454
  mobile: composer.bsEnvironment === 'xs' || composer.bsEnvironment === 'sm',
459
455
  resizable: true,
460
456
  thumb: postData.thumb,
@@ -492,7 +488,7 @@ define('composer', [
492
488
  app.toggleNavbar(false);
493
489
  }
494
490
 
495
- postData.mobile = composer.bsEnvironment === 'xs' || composer.bsEnvironment === 'sm';
491
+ postData.mobile = data.mobile;
496
492
 
497
493
  ({ postData, createData: data } = await hooks.fire('filter:composer.create', {
498
494
  postData: postData,
@@ -505,10 +501,6 @@ define('composer', [
505
501
  }
506
502
  composerTemplate = $(composerTemplate);
507
503
 
508
- composerTemplate.find('.title, textarea.write').each(function () {
509
- $(this).text(translator.unescape($(this).text()));
510
- });
511
-
512
504
  composerTemplate.attr('data-uuid', post_uuid);
513
505
 
514
506
  $(document.body).append(composerTemplate);
@@ -592,7 +584,7 @@ define('composer', [
592
584
  helpBtn.on('click', async function () {
593
585
  const html = await socket.emit('plugins.composer.renderHelp');
594
586
  if (html && html.length > 0) {
595
- bootbox.dialog({
587
+ modals.dialog({
596
588
  size: 'large',
597
589
  message: html,
598
590
  onEscape: true,
@@ -1,5 +1,5 @@
1
1
  <div component="composer" class="composer pb-3 h-100 {{{ if resizable }}} resizable{{{ end }}}{{{ if !isTopicOrMain }}} reply{{{ end }}}"{{{ if !disabled }}} style="visibility: inherit;"{{{ end }}}>
2
- <div class="composer-container d-flex flex-column gap-1 h-100">
2
+ <div class="composer-container d-flex flex-column gap-1 h-100 position-relative">
3
3
  <form id="compose-form" method="post">
4
4
  {{{ if pid }}}
5
5
  <input type="hidden" name="pid" value="{pid}" />
@@ -23,5 +23,7 @@
23
23
  {{{ if isTopicOrMain }}}
24
24
  <!-- IMPORT partials/composer-tags.tpl -->
25
25
  {{{ end }}}
26
+
27
+ <div class="imagedrop"><div>[[topic:composer.drag-and-drop-images]]</div></div>
26
28
  </div>
27
29
  </div>
@@ -1,27 +1,6 @@
1
1
  <div component="composer" class="composer {{{ if resizable }}} resizable{{{ end }}}{{{ if !isTopicOrMain }}} reply{{{ end }}}">
2
- <div class="composer-container d-flex flex-column gap-1 h-100">
3
- <!-- mobile header -->
4
- <nav class="navbar fixed-top mobile-navbar text-bg-primary d-flex d-md-none flex-nowrap gap-1 px-1">
5
- <div class="btn-group">
6
- <button class="btn btn-sm btn-primary composer-discard fs-5" data-action="discard" tabindex="-1"><i class="fa fa-fw fa-times"></i></button>
7
- <button class="btn btn-sm btn-primary composer-minimize fs-5" data-action="minimize" tabindex="-1"><i class="fa fa-fw fa-minus"></i></button>
8
- </div>
9
- {{{ if isTopic }}}
10
- <div class="flex-1" style="min-width: 0px;">
11
- <!-- IMPORT partials/category/selector-dropdown-left.tpl -->
12
- </div>
13
- {{{ end }}}
14
- {{{ if !isTopicOrMain }}}
15
- <h4 class="title text-center text-bg-primary text-truncate fs-6 mb-0 px-2">{titleLabel}</h4>
16
- {{{ end }}}
17
-
18
- <div class="d-flex gap-1 flex-nowrap">
19
- <button class="btn btn-sm btn-primary display-scheduler fs-5 {{{ if !canSchedule }}} hidden{{{ end }}}">
20
- <i class="fa fa-fw fa-clock-o"></i>
21
- </button>
22
- <button class="btn btn-sm btn-primary composer-submit fs-5" data-action="post" tabindex="-1"><i class="fa fa-fw fa-chevron-right"></i></button>
23
- </div>
24
- </nav>
2
+ <div class="composer-container d-flex flex-column gap-1 h-100 position-relative">
3
+ <!-- IMPORT partials/composer-mobile-header.tpl -->
25
4
  <div class="p-2 d-flex flex-column gap-1 h-100">
26
5
  <!-- IMPORT partials/composer-title-container.tpl -->
27
6
 
@@ -4,9 +4,9 @@
4
4
  {{{ if ./spacer }}}
5
5
  <li class="small spacer"></li>
6
6
  {{{ else }}}
7
- {{{ if (./visibility.desktop && ((isTopicOrMain && ./visibility.main) || (!isTopicOrMain && ./visibility.reply))) }}}
7
+ {{{ if ((isTopicOrMain && ./visibility.main) || (!isTopicOrMain && ./visibility.reply)) }}}
8
8
  {{{ if ./dropdownItems.length }}}
9
- <li class="dropdown dropdown-left bottom-sheet" title="{./title}">
9
+ <li class="dropdown dropdown-left bottom-sheet {./visibility.class}" title="{tx(./title)}">
10
10
  <button class="btn btn-sm btn-link text-reset" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="{./title}">
11
11
  <i class="{./className}"></i>
12
12
  </button>
@@ -14,7 +14,7 @@
14
14
  {{{ each ./dropdownItems }}}
15
15
  <li>
16
16
  <a href="#" data-format="{./name}" class="dropdown-item rounded-1 position-relative" role="menuitem">
17
- <i class="{./className} text-secondary"></i> {./text}
17
+ <i class="{./className} text-secondary"></i> {tx(./text)}
18
18
  {{{ if ./badge }}}
19
19
  <span class="px-1 position-absolute top-0 start-100 translate-middle badge rounded text-bg-info"></span>
20
20
  {{{ end }}}
@@ -24,8 +24,8 @@
24
24
  </ul>
25
25
  </li>
26
26
  {{{ else }}}
27
- <li title="{./title}">
28
- <button data-format="{./name}" class="btn btn-sm btn-link text-reset position-relative" aria-label="{./title}">
27
+ <li title="{tx(./title)}" class="{./visibility.class}">
28
+ <button data-format="{./name}" class="btn btn-sm btn-link text-reset position-relative" aria-label="{tx(./title)}">
29
29
  <i class="{./className}"></i>
30
30
  {{{ if ./badge }}}
31
31
  <span class="px-1 position-absolute top-0 start-100 translate-middle badge rounded text-bg-info"></span>
@@ -37,22 +37,6 @@
37
37
  {{{ end }}}
38
38
  {{{ end }}}
39
39
 
40
- {{{ if canUploadImage }}}
41
- <li title="[[modules:composer.upload-picture]]">
42
- <button data-format="picture" class="img-upload-btn btn btn-sm btn-link text-reset" aria-label="[[modules:composer.upload-picture]]">
43
- <i class="fa fa-file-image-o"></i>
44
- </button>
45
- </li>
46
- {{{ end }}}
47
-
48
- {{{ if canUploadFile }}}
49
- <li title="[[modules:composer.upload-file]]">
50
- <button data-format="upload" class="file-upload-btn btn btn-sm btn-link text-reset" aria-label="[[modules:composer.upload-file]]">
51
- <i class="fa fa-file-o"></i>
52
- </button>
53
- </li>
54
- {{{ end }}}
55
-
56
40
  <form id="fileForm" method="post" enctype="multipart/form-data">
57
41
  <input type="file" id="files" name="files[]" multiple class="hide"/>
58
42
  </form>
@@ -0,0 +1,21 @@
1
+ <nav class="navbar fixed-top mobile-navbar text-bg-primary d-flex d-md-none flex-nowrap gap-1 px-1">
2
+ <div class="btn-group">
3
+ <button class="btn btn-sm btn-primary composer-discard fs-5" data-action="discard" tabindex="-1"><i class="fa fa-fw fa-times"></i></button>
4
+ <button class="btn btn-sm btn-primary composer-minimize fs-5" data-action="minimize" tabindex="-1"><i class="fa fa-fw fa-minus"></i></button>
5
+ </div>
6
+ {{{ if isTopic }}}
7
+ <div class="flex-1" style="min-width: 0px;">
8
+ <!-- IMPORT partials/category/selector-dropdown-left.tpl -->
9
+ </div>
10
+ {{{ end }}}
11
+ {{{ if !isTopicOrMain }}}
12
+ <h4 class="title text-center text-bg-primary text-truncate fs-6 mb-0 px-2">{titleLabel}</h4>
13
+ {{{ end }}}
14
+
15
+ <div class="d-flex gap-1 flex-nowrap">
16
+ <button class="btn btn-sm btn-primary display-scheduler fs-5 {{{ if !canSchedule }}} hidden{{{ end }}}">
17
+ <i class="fa fa-fw fa-clock-o"></i>
18
+ </button>
19
+ <button class="btn btn-sm btn-primary composer-submit fs-5" data-action="post" tabindex="-1"><i class="fa fa-fw fa-chevron-right"></i></button>
20
+ </div>
21
+ </nav>
@@ -15,7 +15,15 @@
15
15
  {{{ if isTopicOrMain }}}
16
16
  <input class="title form-control h-100 rounded-1 shadow-none" type="text" placeholder="[[topic:composer.title-placeholder]]" value="{topicTitle}" />
17
17
  {{{ else }}}
18
- <span class="{{{ if !template.compose }}}d-none d-md-block{{{ else }}}d-block{{{ end }}} title h-100 text-truncate">{titleLabel}</span>
18
+ <span class="{{{ if !template.compose }}}d-none d-md-block{{{ else }}}d-block{{{ end }}} title h-100 text-truncate">
19
+ {{{ if (./action == "posts.reply") }}}
20
+ {{tx("topic:composer.replying-to", txEscape(quote(./topicTitle)))}}
21
+ {{{ end }}}
22
+
23
+ {{{ if (./action == "posts.edit") }}}
24
+ {{tx("topic:composer.editing-in", txEscape(quote(./topicTitle)))}}
25
+ {{{ end }}}
26
+ </span>
19
27
  {{{ end }}}
20
28
  <div id="quick-search-container" class="quick-search-container mt-2 dropdown-menu d-block p-2 hidden">
21
29
  <div class="text-center loading-indicator"><i class="fa fa-spinner fa-spin"></i></div>
@@ -37,7 +45,7 @@
37
45
  <li><a class="dropdown-item rounded-1 display-scheduler {{{ if !canSchedule }}}hidden{{{ end }}}" role="menuitem">[[topic:composer.post-later]]</a></li>
38
46
  <li><a class="dropdown-item rounded-1 cancel-scheduling hidden" role="menuitem">[[modules:composer.cancel-scheduling]]</a></li>
39
47
  {{{ each submitOptions }}}
40
- <li><a class="dropdown-item rounded-1" href="#" data-action="{./action}" role="menuitem">{./text}</a></li>
48
+ <li><a class="dropdown-item rounded-1" href="#" data-action="{./action}" role="menuitem">{{./text}}</a></li>
41
49
  {{{ end }}}
42
50
  </ul>
43
51
  </div>
package/websockets.js CHANGED
@@ -1,10 +1,10 @@
1
1
  'use strict';
2
2
 
3
- const meta = require.main.require('./src/meta');
4
- const privileges = require.main.require('./src/privileges');
5
- const posts = require.main.require('./src/posts');
6
- const topics = require.main.require('./src/topics');
7
- const plugins = require.main.require('./src/plugins');
3
+ const meta = nodebb.require('./src/meta');
4
+ const privileges = nodebb.require('./src/privileges');
5
+ const posts = nodebb.require('./src/posts');
6
+ const topics = nodebb.require('./src/topics');
7
+ const plugins = nodebb.require('./src/plugins');
8
8
 
9
9
  const Sockets = module.exports;
10
10
 
@@ -64,8 +64,8 @@ Sockets.renderHelp = async function () {
64
64
  return helpText;
65
65
  };
66
66
 
67
- Sockets.getFormattingOptions = async function () {
68
- return await require('./library').getFormattingOptions();
67
+ Sockets.getFormattingOptions = async function (socket) {
68
+ return await require('./library').getFormattingOptions(socket.uid);
69
69
  };
70
70
 
71
71
  Sockets.shouldQueue = async function (socket, data) {