diff --git a/node_modules/nodebb-plugin-composer-default/static/scss/composer.scss b/node_modules/nodebb-plugin-composer-default/static/scss/composer.scss new file mode 100644 index 0000000000..918ebf2ef5 --- /dev/null +++ b/node_modules/nodebb-plugin-composer-default/static/scss/composer.scss @@ -0,0 +1,417 @@ +.composer { + background-color: var(--bs-body-bg); + color: var(--bs-body-color); + z-index: $zindex-dropdown; + visibility: hidden; + padding: 0; + position: fixed; + bottom: 0; + top: 0; + right: 0; + left: 0; + + .switch { + position: relative ; + display: inline-block; + width: 37px; + height: 18px; + background-color: #9f9f9f; + border-radius: 20px; + } + + .switch::after { + content: ''; + position: absolute; + width: 16px; + height: 16px; + border-radius: 50%; + background-color: white; + top: 1px; + left: 1px; + transition: all 0.2s; + } + + .checkbox:checked + .switch::after { + left: 20px; + } + .checkbox:checked + .switch { + background-color: #383839; + } + .checkbox { + display: none; + } + + + .mobile-navbar { + position: static; + min-height: 40px; + margin: 0; + + .btn-group { + flex-shrink: 0; + } + + button { + font-size: 20px; + } + + display: flex; + + .category-name-container, .title { + text-align: center; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + flex-grow: 2; + font-size: 16px; + line-height: inherit; + padding: 9px 5px; + margin: 0; + } + } + + .title-container { + > div[data-component="composer/handle"] { + flex: 0.33; + } + + .category-list-container { + + [component="category-selector"] { + .category-dropdown-menu { + max-height: 300px; + } + } + } + + .category-list { + padding: 0 2rem; + } + + .action-bar { + .dropdown-menu:empty { + & ~ .dropdown-toggle { + display: none; + } + } + } + } + + .formatting-bar { + .spacer { + &:before { + content: ' | '; + color: $gray-200; + } + } + } + + .tags-container { + [component="composer/tag/dropdown"] { + .dropdown-menu { + max-height: 400px; + overflow-y: auto; + } + + > button { + border: 0; + } + } + // if picking tags from taglist dropdown hide the input + &.haswhitelist .bootstrap-tagsinput { + input { + display: none; + } + } + .bootstrap-tagsinput { + background: transparent; + flex-grow: 1; + border: 0; + padding: 0; + box-shadow: none; + max-height: 80px; + overflow: auto; + + input { + &::placeholder{ + color: $input-placeholder-color; + } + color: $body-color; + font-size: 16px; + width: 50%; + @include media-breakpoint-down(md) { + width: 100%; + } + + + height: 28px; + padding: 4px 6px; + } + + .ui-autocomplete { + max-height: 350px; + overflow-x: hidden; + overflow-y: auto; + } + } + } + + .resizer { + background: linear-gradient(transparent, var(--bs-body-bg)); + margin-left: calc($spacer * -0.5); + padding-left: $spacer; + + .trigger { + cursor: ns-resize; + + .handle { + border-top-left-radius: 50%; + border-top-right-radius: 50%; + border-bottom: 0 !important; + } + } + } + + .minimize { + display: none; + position: absolute; + top: 0px; + right: 10px; + height: 0; + + @include pointer; + + .trigger { + position: relative; + display: block; + top: -20px; + right: 0px; + margin: 0 auto; + margin-left: 20px; + line-height: 26px; + @include transition(filter .15s linear); + + &:hover { + filter: invert(100%); + } + + i { + width: 32px; + height: 32px; + background: #333; + border: 1px solid #333; + border-radius: 50%; + + position: relative; + + color: #FFF; + font-size: 16px; + + &:before { + position: relative; + top: 25%; + } + } + } + } + + &.reply { + .title-container { + display: none; + } + } + + &.resizable.maximized { + .resizer { + top: 0 !important; + background: transparent; + + .trigger { + height: $spacer * 0.5; + + .handle { + border-top-left-radius: 0%; + border-top-right-radius: 0%; + border-bottom-left-radius: 50%; + border-bottom-right-radius: 50%; + border-bottom: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; + } + + i { + &:before { + content: fa-content($fa-var-chevron-down); + } + } + } + } + } + + .draft-icon { + font-family: 'FontAwesome'; + color: $success; + opacity: 0; + + &::before { + content: fa-content($fa-var-save); + } + + &.active { + animation: draft-saved 3s ease; + } + } + + textarea { + resize: none; + } + + .preview { + padding: $input-padding-y $input-padding-x; + } +} + +.datetime-picker { + display: flex; + justify-content: center; + flex-direction: row; + min-width: 310px; + max-width: 310px; + margin: 0 auto; + + input { + flex: 3; + line-height: inherit; + } + + input + input { + border-left: none; + flex: 2; + } +} + +.modal.topic-scheduler { + z-index: 1070; + & + .modal-backdrop { + z-index: 1060; + } +} + +@keyframes draft-saved { + 0%, 100% { + opacity: 0; + } + + 15% { + opacity: 1; + } + + 30% { + opacity: 0.5; + } + + 45% { + opacity: 1; + } + + 85% { + opacity: 1; + } +} + +@keyframes pulse { + from { + transform: scale(1); + color: inherit; + } + 50% { + transform: scale(.9); + } + to { + transform: scale(1); + color: #00adff; + } +} + +@include media-breakpoint-down(lg) { + html.composing .composer { z-index: $zindex-modal; } +} + +@include media-breakpoint-down(sm) { + html.composing { + .composer { + height: 100%; + + .draft-icon { + position: absolute; + bottom: 1em; + right: 0em; + + &::after { + top: 7px; + } + } + + .preview-container { + max-width: initial; + } + } + + body { + padding-bottom: 0 !important; + } + } +} + +@include media-breakpoint-up(lg) { + html.composing { + .composer { + left: 15%; + width: 70%; + min-height: 400px; + + .resizer { + display: block; + } + + .minimize { + display: block; + } + } + } +} + +@include media-breakpoint-up(md) { + // without this formatting elements that are dropdowns are not visible on desktop. + // on mobile dropdowns use bottom-sheet and overflow is auto + .formatting-group { + overflow: visible!important; + } +} + +@import './zen-mode'; +@import './page-compose'; +@import './textcomplete'; + + +.skin-noskin, .skin-cosmo, .skin-flatly, +.skin-journal, .skin-litera, .skin-minty, .skin-pulse, +.skin-sandstone, .skin-sketchy, .skin-spacelab, .skin-united { + .composer { + color: var(--bs-secondary) !important; + background-color: var(--bs-light) !important; + } +} + +.skin-cerulean, .skin-lumen, .skin-lux, .skin-morph, +.skin-simplex, .skin-yeti, .skin-zephyr { + .composer { + color: var(--bs-body) !important; + background-color: var(--bs-light) !important; + } +} + +@include color-mode(dark) { + .skin-noskin .composer { + color: var(--bs-secondary)!important; + background-color: var(--bs-body-bg)!important; + } +} \ No newline at end of file diff --git a/node_modules/nodebb-plugin-composer-default/static/templates/partials/composer-formatting.tpl b/node_modules/nodebb-plugin-composer-default/static/templates/partials/composer-formatting.tpl new file mode 100644 index 0000000000..5ad195e416 --- /dev/null +++ b/node_modules/nodebb-plugin-composer-default/static/templates/partials/composer-formatting.tpl @@ -0,0 +1,84 @@ +
+ +
+ + + + + + + + {{{ if composer:showHelpTab }}} + + {{{ end }}} +
+
+ diff --git a/package 2.json b/package 2.json new file mode 100644 index 0000000000..cb5eb4e4ea --- /dev/null +++ b/package 2.json @@ -0,0 +1,198 @@ +{ + "name": "nodebb", + "license": "GPL-3.0", + "description": "NodeBB Forum", + "version": "3.8.4", + "homepage": "https://www.nodebb.org", + "repository": { + "type": "git", + "url": "https://github.com/NodeBB/NodeBB/" + }, + "main": "app.js", + "scripts": { + "start": "node loader.js", + "lint": "eslint --cache ./nodebb .", + "test": "nyc --reporter=html --reporter=text-summary mocha", + "coverage": "nyc report --reporter=text-lcov > ./coverage/lcov.info", + "coveralls": "nyc report --reporter=text-lcov | coveralls && rm -r coverage" + }, + "nyc": { + "exclude": [ + "src/upgrades/*", + "test/*" + ] + }, + "lint-staged": { + "*.js": [ + "eslint --fix" + ] + }, + "dependencies": { + "@adactive/bootstrap-tagsinput": "0.8.2", + "@fontsource/inter": "5.0.18", + "@fontsource/poppins": "5.0.14", + "@fortawesome/fontawesome-free": "6.5.2", + "@isaacs/ttlcache": "1.4.1", + "@nodebb/spider-detector": "2.0.3", + "@popperjs/core": "2.11.8", + "ace-builds": "1.33.2", + "archiver": "7.0.1", + "async": "3.2.5", + "autoprefixer": "10.4.19", + "bcryptjs": "2.4.3", + "benchpressjs": "2.5.1", + "body-parser": "1.20.2", + "bootbox": "6.0.0", + "bootstrap": "5.3.3", + "bootswatch": "5.3.3", + "chalk": "4.1.2", + "chart.js": "4.4.2", + "cli-graph": "3.2.2", + "clipboard": "2.0.11", + "colors": "1.4.0", + "commander": "12.0.0", + "compare-versions": "6.1.0", + "compression": "1.7.4", + "connect-flash": "0.1.1", + "connect-mongo": "5.1.0", + "connect-multiparty": "2.2.0", + "connect-pg-simple": "9.0.1", + "connect-redis": "7.1.1", + "cookie-parser": "1.4.6", + "cron": "3.1.7", + "cropperjs": "1.6.2", + "csrf-sync": "4.0.3", + "daemon": "1.1.0", + "diff": "5.2.0", + "esbuild": "0.21.2", + "express": "4.19.2", + "express-session": "1.18.0", + "express-useragent": "1.0.15", + "fetch-cookie": "3.0.1", + "file-loader": "6.2.0", + "fs-extra": "11.2.0", + "graceful-fs": "4.2.11", + "helmet": "7.1.0", + "html-to-text": "9.0.5", + "imagesloaded": "5.0.0", + "ipaddr.js": "2.2.0", + "jquery": "3.7.1", + "jquery-deserialize": "2.0.0", + "jquery-form": "4.3.0", + "jquery-serializeobject": "1.0.0", + "jquery-ui": "1.13.3", + "jsesc": "3.0.2", + "json2csv": "5.0.7", + "jsonwebtoken": "9.0.2", + "lodash": "4.17.21", + "logrotate-stream": "0.2.9", + "lru-cache": "10.2.2", + "mime": "3.0.0", + "mkdirp": "3.0.1", + "mongodb": "6.6.1", + "morgan": "1.10.0", + "mousetrap": "1.6.5", + "multiparty": "4.2.3", + "nconf": "0.12.1", + "nodebb-plugin-2factor": "7.5.3", + "nodebb-plugin-composer-default": "10.2.36", + "nodebb-plugin-dbsearch": "6.2.5", + "nodebb-plugin-emoji": "5.1.15", + "nodebb-plugin-emoji-android": "4.0.0", + "nodebb-plugin-markdown": "12.2.6", + "nodebb-plugin-mentions": "4.4.3", + "nodebb-plugin-ntfy": "1.7.4", + "nodebb-plugin-spam-be-gone": "2.2.2", + "nodebb-rewards-essentials": "1.0.0", + "nodebb-theme-harmony": "1.2.63", + "nodebb-theme-lavender": "7.1.8", + "nodebb-theme-peace": "2.2.6", + "nodebb-theme-persona": "13.3.25", + "nodebb-widget-essentials": "7.0.18", + "nodemailer": "6.9.13", + "nprogress": "0.2.0", + "passport": "0.7.0", + "passport-http-bearer": "1.0.1", + "passport-local": "1.0.0", + "pg": "8.11.5", + "pg-cursor": "2.10.5", + "postcss": "8.4.38", + "postcss-clean": "1.2.0", + "progress-webpack-plugin": "1.0.16", + "prompt": "1.3.0", + "ioredis": "5.4.1", + "rimraf": "5.0.7", + "rss": "1.2.2", + "rtlcss": "4.1.1", + "sanitize-html": "2.13.0", + "sass": "1.77.1", + "semver": "7.6.2", + "serve-favicon": "2.5.0", + "sharp": "0.32.6", + "sitemap": "7.1.1", + "socket.io": "4.7.5", + "socket.io-client": "4.7.5", + "@socket.io/redis-adapter": "8.3.0", + "sortablejs": "1.15.2", + "spdx-license-list": "6.9.0", + "terser-webpack-plugin": "5.3.10", + "textcomplete": "0.18.2", + "textcomplete.contenteditable": "0.1.1", + "timeago": "1.6.7", + "tinycon": "0.6.8", + "toobusy-js": "0.5.1", + "tough-cookie": "4.1.4", + "validator": "13.12.0", + "webpack": "5.91.0", + "webpack-merge": "5.10.0", + "winston": "3.13.0", + "workerpool": "9.1.1", + "xml": "1.0.1", + "xregexp": "5.1.1", + "yargs": "17.7.2", + "zxcvbn": "4.4.2" + }, + "devDependencies": { + "@apidevtools/swagger-parser": "10.1.0", + "@commitlint/cli": "19.3.0", + "@commitlint/config-angular": "19.3.0", + "coveralls": "3.1.1", + "eslint": "8.57.0", + "eslint-config-nodebb": "0.2.1", + "eslint-plugin-import": "2.29.1", + "grunt": "1.6.1", + "grunt-contrib-watch": "1.1.0", + "husky": "8.0.3", + "jsdom": "24.0.0", + "lint-staged": "15.2.2", + "mocha": "10.4.0", + "mocha-lcov-reporter": "1.3.0", + "mockdate": "3.0.5", + "nyc": "15.1.0", + "smtp-server": "3.13.4" + }, + "optionalDependencies": { + "sass-embedded": "1.77.1" + }, + "resolutions": { + "*/jquery": "3.7.1" + }, + "bugs": { + "url": "https://github.com/NodeBB/NodeBB/issues" + }, + "engines": { + "node": ">=18" + }, + "maintainers": [ + { + "name": "Julian Lam", + "email": "julian@nodebb.org", + "url": "https://github.com/julianlam" + }, + { + "name": "Barış Soner Uşaklı", + "email": "baris@nodebb.org", + "url": "https://github.com/barisusakli" + } + ] +} \ No newline at end of file diff --git a/pidfile 2 b/pidfile 2 new file mode 100644 index 0000000000..a030d98b27 --- /dev/null +++ b/pidfile 2 @@ -0,0 +1 @@ +9553 \ No newline at end of file diff --git a/public/openapi/components/schemas/PostObject.yaml b/public/openapi/components/schemas/PostObject.yaml index ea91579cc6..138cb6a4e7 100644 --- a/public/openapi/components/schemas/PostObject.yaml +++ b/public/openapi/components/schemas/PostObject.yaml @@ -139,4 +139,4 @@ PostObject: isMainPost: type: boolean replies: - type: number \ No newline at end of file + type: number diff --git a/public/openapi/write/posts/pid.yaml b/public/openapi/write/posts/pid.yaml index 593a7acd01..6c82211fc2 100644 --- a/public/openapi/write/posts/pid.yaml +++ b/public/openapi/write/posts/pid.yaml @@ -52,6 +52,8 @@ get: type: number bookmarks: type: number + anonymous: + type: number votes: type: number timestampISO: @@ -138,4 +140,4 @@ delete: $ref: ../../components/schemas/Status.yaml#/Status response: type: object - properties: {} \ No newline at end of file + properties: {} diff --git a/public/src/client/topic.js b/public/src/client/topic.js index 7e65cbeb4f..489219a7ac 100644 --- a/public/src/client/topic.js +++ b/public/src/client/topic.js @@ -73,7 +73,6 @@ define('forum/topic', [ $(window).on('scroll', utils.debounce(updateTopicTitle, 250)); handleTopicSearch(); - hooks.fire('action:topic.loaded', ajaxify.data); }; diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js index f8d2ca8933..c593f1ede9 100644 --- a/public/src/client/topic/postTools.js +++ b/public/src/client/topic/postTools.js @@ -267,7 +267,6 @@ define('forum/topic/postTools', [ openChat($(this)); }); } - async function onReplyClicked(button, tid) { const selectedNode = await getSelectedNode(); diff --git a/src/api/posts.js b/src/api/posts.js index 4e3917a008..91f92f2981 100644 --- a/src/api/posts.js +++ b/src/api/posts.js @@ -34,7 +34,6 @@ postsAPI.get = async function (caller, data) { Object.assign(post, voted); post.ip = userPrivilege.isAdminOrMod ? post.ip : undefined; - const selfPost = caller.uid && caller.uid === parseInt(post.uid, 10); if (post.deleted && !(userPrivilege.isAdminOrMod || selfPost)) { post.content = '[[topic:post-is-deleted]]'; diff --git a/src/api/topics.js b/src/api/topics.js index 7a6cabf966..baa572d128 100644 --- a/src/api/topics.js +++ b/src/api/topics.js @@ -54,8 +54,7 @@ topicsAPI.create = async function (caller, data) { if (!data) { throw new Error('[[error:invalid-data]]'); } - - const payload = { ...data }; + const payload = { ...data, anonymous: data.anonymous || false }; payload.tags = payload.tags || []; apiHelpers.setDefaultPostData(caller, payload); const isScheduling = parseInt(data.timestamp, 10) > payload.timestamp; @@ -72,7 +71,6 @@ topicsAPI.create = async function (caller, data) { if (shouldQueue) { return await posts.addToQueue(payload); } - const result = await topics.post(payload); await topics.thumbs.migrate(data.uuid, result.topicData.tid); diff --git a/src/api/users.js b/src/api/users.js index c4f4add772..7afa94596d 100644 --- a/src/api/users.js +++ b/src/api/users.js @@ -34,7 +34,6 @@ usersAPI.create = async function (caller, data) { throw new Error('[[error:invalid-data]]'); } await hasAdminPrivilege(caller.uid, 'users'); - const uid = await user.create(data); return await user.getUserData(uid); }; diff --git a/src/posts/data.js b/src/posts/data.js index 3a4d303ff5..88dea32658 100644 --- a/src/posts/data.js +++ b/src/posts/data.js @@ -7,7 +7,7 @@ const utils = require('../utils'); const intFields = [ 'uid', 'pid', 'tid', 'deleted', 'timestamp', 'upvotes', 'downvotes', 'deleterUid', 'edited', - 'replies', 'bookmarks', + 'replies', 'bookmarks', 'anonymous', ]; module.exports = function (Posts) { @@ -42,6 +42,9 @@ module.exports = function (Posts) { Posts.getPostFields = async function (pid, fields) { const posts = await Posts.getPostsFields([pid], fields); + if (!fields.includes('anonymous')) { + fields.push('anonymous'); + } return posts ? posts[0] : null; }; diff --git a/src/topics/data.js b/src/topics/data.js index 1260c092e1..903d314e38 100644 --- a/src/topics/data.js +++ b/src/topics/data.js @@ -16,6 +16,7 @@ const intFields = [ ]; module.exports = function (Topics) { + console.log(Topics); Topics.getTopicsFields = async function (tids, fields) { if (!Array.isArray(tids) || !tids.length) { return []; diff --git a/src/topics/posts.js b/src/topics/posts.js index 73eb29b9f9..e1a9844acc 100644 --- a/src/topics/posts.js +++ b/src/topics/posts.js @@ -1,4 +1,3 @@ - 'use strict'; const _ = require('lodash'); @@ -12,6 +11,7 @@ const meta = require('../meta'); const plugins = require('../plugins'); const utils = require('../utils'); + const backlinkRegex = new RegExp(`(?:${nconf.get('url').replace('/', '\\/')}|\b|\\s)\\/topic\\/(\\d+)(?:\\/\\w+)?`, 'g'); module.exports = function (Topics) { @@ -104,6 +104,8 @@ module.exports = function (Topics) { } Topics.addPostData = async function (postData, uid) { + const isAnonymous = await posts.getPostField(postData, 'anonymous'); + console.log('anonymous', isAnonymous); if (!Array.isArray(postData) || !postData.length) { return []; } @@ -128,7 +130,6 @@ module.exports = function (Topics) { getPostReplies(postData, uid), Topics.addParentPosts(postData), ]); - postData.forEach((postObj, i) => { if (postObj) { postObj.user = postObj.uid ? userData[postObj.uid] : { ...userData[postObj.uid] }; @@ -139,11 +140,13 @@ module.exports = function (Topics) { postObj.votes = postObj.votes || 0; postObj.replies = replies[i]; postObj.selfPost = parseInt(uid, 10) > 0 && parseInt(uid, 10) === postObj.uid; - // Username override for guests, if enabled if (meta.config.allowGuestHandles && postObj.uid === 0 && postObj.handle) { postObj.user.username = validator.escape(String(postObj.handle)); postObj.user.displayname = postObj.user.username; + } else if (isAnonymous) { + postObj.user.username = 'anonymous'; + postObj.user.displayname = 'anonymous'; } } }); diff --git a/test/api.js b/test/api.js index 0ea9918953..9f7a40143f 100644 --- a/test/api.js +++ b/test/api.js @@ -518,7 +518,19 @@ describe('API', async () => { `${method.toUpperCase()} ${path} sent back unexpected HTTP status code: ${result.response.statusCode}` ); }); - + it('response for get post requests should include anonymous attribute', () => { + if (method === 'get' && path.startsWith('/api/v3/posts')) { + const http200 = context[method].responses['200']; + if (http200 && result.response.statusCode === 200) { + const hasJSON = http200.content && http200.content['application/json']; + if (hasJSON) { + result.body.forEach((post) => { + assert(post.hasOwnProperty('anonymous'), "Post should have an 'anonymous' attribute"); + }); + } + } + } + }); // Recursively iterate through schema properties, comparing type it('response body should match schema definition', () => { const http302 = context[method].responses['302'];