diff --git a/CHANGELOG.md b/CHANGELOG.md index 13fce6fb89..1dec06f21f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,414 @@ +#### v4.9.0 (2026-02-27) + +##### Chores + +* up peace (cf0f2923) +* up peace (0b3dd38b) +* up peace (e4b15e05) +* up peace (64a3c822) +* up tdbsearch (50f5541e) +* up harmony (44f663b1) +* up harmony (fd2c9927) +* up harmony (066470b1) +* up harmony (bfa8d5aa) +* up harmony (df3de36c) +* up harmony (c1581a12) +* up mentions (537d3676) +* up harmony (41ef71fa) +* up themes (38680c3e) +* up harmony (6da00614) +* up persona (033f9198) +* up themes (84ec8ef9) +* up harmony (f818dd96) +* up themes (bac67399) +* up harmony (dd84a303) +* up composer (41bc49f7) +* up themes, add hide to alert (824a9b77) +* update harmony (b061d078) +* up composer (d0cc1c95) +* white space (3dfd9a43) +* up themes (97917103) +* up composer (1543650a) +* up themes (47217803) +* up composer (8423da04) +* up themes (a84464cf) +* up themes (bafd5db0) +* up mentions (bc1fd892) +* up mentions (0fd8200a) +* up mentions (9cd87fca) +* up mentions/composer (06f4f700) +* up link-preview (4aac6562) +* forcibly resetting all translations for custom-reason.json (c2695d89) +* up harmony (d6b7f27c) +* new fallbacks (1d17352f) +* up harmony (72510734) +* up harmony (2a5ab6dd) +* up harmony (7d4a440a) +* up markdown (86c62708) +* up composer (3de603f6) +* up deps (b3807656) +* up deps (a241c624) +* up composer (f06557b7) +* harmony (118ceb72) +* up harmony (f2795753) +* up harmony (5c3f2651) +* up composer & harmony (931ae67d) +* up harmony (e6737941) +* up harmony (8d6b6f6a) +* incrementing version number - v4.8.1 (713ae0c0) +* update changelog for v4.8.1 (f53aab43) +* incrementing version number - v4.8.0 (3fac737a) +* incrementing version number - v4.7.2 (cd419d8a) +* incrementing version number - v4.7.1 (afb88805) +* incrementing version number - v4.7.0 (e82d40f8) +* incrementing version number - v4.6.3 (9fc5b0f3) +* incrementing version number - v4.6.2 (f98747db) +* incrementing version number - v4.6.1 (f47aa678) +* incrementing version number - v4.6.0 (ee395bc5) +* incrementing version number - v4.5.2 (ad2da639) +* incrementing version number - v4.5.1 (69f4b61f) +* incrementing version number - v4.5.0 (f05c5d06) +* incrementing version number - v4.4.6 (074043ad) +* incrementing version number - v4.4.5 (6f106923) +* incrementing version number - v4.4.4 (d323af44) +* incrementing version number - v4.4.3 (d354c2eb) +* incrementing version number - v4.4.2 (55c510ae) +* incrementing version number - v4.4.1 (5ae79b4e) +* incrementing version number - v4.4.0 (0a75eee3) +* incrementing version number - v4.3.2 (b92b5d80) +* incrementing version number - v4.3.1 (308e6b9f) +* incrementing version number - v4.3.0 (bff291db) +* incrementing version number - v4.2.2 (17fecc24) +* incrementing version number - v4.2.1 (852a270c) +* incrementing version number - v4.2.0 (87581958) +* incrementing version number - v4.1.1 (b2afbb16) +* incrementing version number - v4.1.0 (36c80850) +* incrementing version number - v4.0.6 (4a52fb2e) +* incrementing version number - v4.0.5 (1792a62b) +* incrementing version number - v4.0.4 (b1125cce) +* incrementing version number - v4.0.3 (2b65c735) +* incrementing version number - v4.0.2 (73fe5fcf) +* incrementing version number - v4.0.1 (a461b758) +* incrementing version number - v4.0.0 (c1eaee45) +* **i18n:** + * fallback strings for new resources: nodebb.admin-manage-categories (a797090f) + * fallback strings for new resources: nodebb.notifications, nodebb.admin-menu, nodebb.admin-settings-activitypub (bde5e0b4) + * fallback strings for new resources: nodebb.admin-menu, nodebb.admin-settings-activitypub (b43fadd0) + * fallback strings for new resources: nodebb.admin-menu (7b71252e) + * fallback strings for new resources: nodebb.admin-dashboard (3f5dafd0) + * fallback strings for new resources: nodebb.admin-settings-uploads (aa4f4808) + * fallback strings for new resources: nodebb.notifications (c84a86e4) + * fallback strings for new resources: nodebb.notifications, nodebb.user (41e7c144) + * fallback strings for new resources: nodebb.admin-manage-users (c3315063) + * fallback strings for new resources: nodebb.notifications (9524359d) + * fallback strings for new resources: nodebb.topic (a4369b93) + * fallback strings for new resources: nodebb.error, nodebb.user (0b9efa85) + * fallback strings for new resources: nodebb.admin-manage-privileges (7f6fcd05) + * fallback strings for new resources: nodebb.topic (781a900c) + * fallback strings for new resources: nodebb.admin-advanced-cache (64dad9db) + * fallback strings for new resources: nodebb.admin-advanced-cache (f0fb661c) + * fallback strings for new resources: nodebb.admin-settings-chat, nodebb.admin-settings-notifications (756e2434) + * fallback strings for new resources: nodebb.groups (d133f910) + * fallback strings for new resources: nodebb.groups (4bccc311) + * fallback strings for new resources: nodebb.admin-settings-web-crawler (e4455b1c) + * fallback strings for new resources: nodebb.world (b527cb5a) + * fallback strings for new resources: nodebb.admin-manage-users, nodebb.notifications (b04d2dbc) + * fallback strings for new resources: nodebb.admin-manage-users (2c0a60c4) + * fallback strings for new resources: nodebb.category, nodebb.world (5bf6b335) + * fallback strings for new resources: nodebb.notifications (2a8b6d44) + * fallback strings for new resources: nodebb.notifications (304a2ab1) + * fallback strings for new resources: nodebb.notifications (3c6804d4) + * fallback strings for new resources: nodebb.modules, nodebb.notifications (317bcd89) + * fallback strings for new resources: nodebb.admin-development-info (54743724) + * fallback strings for new resources: nodebb.admin-settings-uploads (f8d6c4e8) + * fallback strings for new resources: nodebb.topic (77eef491) + * fallback strings for new resources: nodebb.themes-harmony (cc2772ba) + * fallback strings for new resources: nodebb.admin-settings-email (c26698d6) + * fallback strings for new resources: nodebb.error (aaa9570e) + * fallback strings for new resources: nodebb.topic (dce82aae) +* **deps:** + * update redis docker tag to v8.6.1 (#14009) (898c3f1d) + * update commitlint monorepo to v20.4.2 (#14005) (1ae5041f) + * update dependency @stylistic/eslint-plugin to v5.9.0 (#14006) (efae106e) + * update dependency jsdom to v28.1.0 (#13992) (ec8e547c) + * update postgres docker tag to v18.2 (#13987) (25f866ca) + * update redis docker tag to v8.6.0 (#13976) (bb5e7118) + * update dependency @stylistic/eslint-plugin to v5.8.0 (#13965) (c61326df) + * update dependency jsdom to v28 (#13947) (b61fa426) + * update commitlint monorepo to v20.4.1 (#13945) (a9042602) + * update commitlint monorepo to v20.4.0 (#13938) (531b8374) + * update dependency smtp-server to v3.18.1 (#13936) (d8595d69) + * update dependency @stylistic/eslint-plugin to v5.7.1 (#13920) (b036034a) + * update dependency sass-embedded to v1.97.3 (#13921) (75a04aed) + +##### Documentation Changes + +* added new ACP routes (82b7f429) +* add missing privileges prop to world schema (4ef9d5fa) +* add bookmarks to postobject in /world (07f9eda9) +* add bookmarks to postobject (9a15b571) +* chat teasers are different (91323dce) +* add teaser to postobject schema (0178e4fb) +* OpenAPI schema for rules re-ordering route (072dd1ae) + +##### New Features + +* support the magic break string '[...]' in content, such that if found, outbound federation will use all content up to (and including) that string in its summary (cd357aaf) +* add a '[...]' to the end of the generated summary when federating out articles (fep-b2b8) (bf0f5228) +* prune topics remote cids, closes #13461 (20eb02f1) +* change notif controls to toggles (523e3b49) +* add invitedBy to user info page, closes #13972, closes #13997 (1dae3d22) +* quick create on world page (5df2b8b7) +* allow configuring unreadCutoff per user, closes #6811 (8c6ce198) +* introduce new topics:crosspost privilege (5c35dc86) +* add guest-cta.tpl and lang strings (9da67474) +* track all caches created in acp (9ac507e5) +* closes #5867, dont email if user already read notification (a55651d1) +* allow re-ordering of auto-categorization rules (fd43368a) +* add group tx key (45ff9f0d) +* eslint10 (#13967) (62d88555) +* closes #13968, add sitemap cache duration (fe35ad4f) +* add data-field values from tpl into search dict (abcfc1a5) +* closes https://github.com/NodeBB/NodeBB/pull/11970 (007efc21) +* closes #13961, rename ban-reasons to custom reasons (0eaf2bee) +* ban/mute reasons (#13960) (d086ed2c) +* show bodyLong in notifications, closes #4767 (30541a96) +* allow converting pasted images, closes #10352 (472a8fc1) +* add language key for untitled topic draft (0125ab55) +* add missing lang key for light/dark (870c6310) +* closes #13009, add dedicated test smtp button (c8488012) +* closes #13203, make users room owners on private chats (61662f19) +* topic crossposts generate topic events, #13908 (0c79eaa5) +* remote account banning, #13904 (560ad81f) +* opportunistic backfill, #13895 (33c2de9c) + +##### Bug Fixes + +* #14003, set ACAO header on webfinger responses (415602d8) +* restore coveralls script, update gh action to ignore failures (3fab2074) +* change && to ; (95ea376a) +* add `--no-fail` flag to coveralls invocation (c6ddef67) +* dont show /register page after req.session.forceLogin is set (0ef27187) +* escape fullname in chatWithMessage (64a072c9) +* send guests to login page if they access uncategorized topics without local posts (b4f8e20b) +* remove allowed check from notes.assert as it is already done downstream in topics.(post|reply), update privilege check to inherit world privs if passed-in cid is remote cid (64724629) +* NaN on ap post delete (53b208b7) +* guard against crash when malformed URL present in deliverees (6a0663cd) +* remove unused ACP route (f7d87ecb) +* remove unused ACP controller, add instance count to federation > safety (a35b7d73) +* allow break string and summary limits to be defined and applied (29111ba7) +* make tables responsive on relays/rules pages (0071216c) +* don't show magic break string in regularly parsed posts, tests for ap mocks lib (b460506e) +* remove `preview` in mocked note for now, due to lack of support (fep-b2b8) (31773694) +* topic purge (e484899d) +* upgrade script, dont remove while processing zset (7a23e291) +* dont store ap tids in topics:tid (e8ef2e5c) +* closes #14010 (003e6d07) +* hook name (d6cf5fba) +* add missing timestamp (68eba728) +* gate /world behind ensureLoggedIn middleware (bcfdbf7c) +* #14007, deny access for guests to topics in cid -1, unless a post from a local user exists (de4f016f) +* closes #7221 restrict cropperjs box to profileImageDimension form ACP (f2bbf369) +* category search shoudn't return results that match in the cid part (75477202) +* favicon url (1a35131a) +* allow passing a selected category markup (309d3003) +* overflowing images on /world (438b4f62) +* missing cb in world IS handler (ab62a8e4) +* closes #14002, add max-height (9c5ffe36) +* fallbacks and latest translations for nodebb.user (053ce073) +* keep chat input in view after adding new messages (2f88f776) +* #14001, regression from adjusted acceptable types list (80f61022) +* don't publish name on generated titles (9fbdc792) +* closes #13999, delay cache creation (42362ccf) +* when registering through an invite, prepopulate the email field on /register/complete with the email (2015777f) +* bump persona (a68311de) +* bump harmony for world page changes (e76f8a60) +* only call syncUserInboxes on post create if local uid creates post in cid -1 (45d2e628) +* schema... not sure why I need this all of a sudden (3e2070b2) +* minor cleanup of quick-reply args; opts.body (a9c2457f) +* render new post in feed when posting via quick create (eb0aa6d8) +* call syncUserInboxes asyncronously (5da35bda) +* update quickreply.init so that it can be passed an options parameter, generate proper draft id for world page (94df9738) +* lint, unused class (4bf0f61e) +* increase categories sent to 1k closes #12841 (3c08b730) +* #13993, encodeURICompoent pid since it can be AP url (b607a80a) +* wrong wrapping of route (bb9033af) +* dont call getInbox for /recent (1ca9841c) +* #13990, don't blindly set `user` field on notification objects (8c8782fd) +* unbans not triggering if user data is loaded wit 'banned' property only (0b7df274) +* #13894, buildCategoryLabel helper checks fa-nbb-none (4b9b3648) +* missing gap (e16c5677) +* gate crossposting behind new topics:crosspost privilege (fe8fd9d6) +* #13983, show only local categories in ACP privilege selector (c4411423) +* add example value for failing schema test (292e70f7) +* lint (1598004e) +* bad relative path (7eb49136) +* regression from refactor of uploadedpicture refactor (b95cd882) +* organize rules and relays logic to separate methods (78d7130c) +* #13969, bump mentions (4a38d67c) +* lint (9ebd8f4f) +* #13962, infinite scroll and pagination not working on world (9f1369a2) +* bump themes, l10n world sort label (76fe4bdd) +* rename translations as well (433d318f) +* cant store URL in nconf (6cb6cf7d) +* update tx config (1e109c2e) +* derpy api page (aef0bd97) +* guard against incomplete objects when building context/chain (13422bc8) +* closes #13953, show uid pic in post queue notification (a8a1089e) +* remote post notifs missing bodyLong (18c04d34) +* double-ajaxify on socket connect (381334f4) +* dont update teaser for public chats (149d649a) +* acp graph labels, dont use indices (17bfd73e) +* closes #8642, stricter username check (94885109) +* if there is no hr create one (f249699d) +* closes #13240, move the updatedTeaser to the top of the recent chat list (0a9c5d30) +* regression with updateHistory (b7ea2767) +* #13939, dont append / if url is empty (2dc49c82) +* acp category selector when category has image (ef75f1ba) +* pagination always getting set to default (eab4025b) +* folder name (f05f8b63) +* proper attachment generation on replies, fixed replies getting thumb attachment when it wasn't part of it (69c5f941) +* closes #13734, set process.env.NODE_ENV early using argv (252d1d09) +* update buildRecipents to add option to skip target creation step, update ap actors for note to not bother building targets (ad27347f) +* notes announce cache, use cache when retrieving tid posters (eb27b964) +* optimizations (e697d600) +* cache detection logic in context parseItem executing earlier than needed causing false positives (f9affbad) +* closes #12458, on socket.io reconnect (13bf64c9) +* restrict topic backfill to logged-in users when browsing to a category (0262bb83) +* replace attachment generation logic in notes.public (428b6e73) +* #13900, assertion re-index (6383bb58) +* simplicity tweak (39582cbd) +* export sendMessage as _sendMessage for use in ap jobs lib (4bab9fb4) +* #13892, logical flaw (8b7d350e) +* **i18n:** + * global fallbacks again (955579fa) + * 'global' resource fallbacks (c47acec9) +* **deps:** + * update dependency sanitize-html to v2.17.1 (#14004) (096e9a3e) + * update dependency redis to v5.11.0 (#13996) (63199ea7) + * update dependency rimraf to v6.1.3 (#13994) (b40f5c7b) + * update dependency qs to v6.15.0 (#13995) (5f82e56b) + * update dependency sortablejs to v1.15.7 (#13985) (71d4a6fc) + * update dependency nodebb-plugin-composer-default to v10.3.16 (#13991) (ff292f7d) + * update dependency webpack to v5.105.2 (#13986) (1020092b) + * update dependency qs to v6.14.2 (#13978) (6e4e02a6) + * update dependency lru-cache to v11.2.6 (#13970) (2cddaf86) + * update dependency satori to v0.19.2 (#13974) (c4420da3) + * update dependency webpack to v5.105.1 (#13975) (48929aae) + * update dependency esbuild to v0.27.3 (#13957) (15ba76e3) + * update dependency semver to v7.7.4 (#13958) (fe66c812) + * update dependency nodemailer to v8 (#13951) (ab60c39c) + * update dependency mongodb to v7.1.0 (#13950) (85e99d6b) + * update dependency webpack to v5.105.0 (#13949) (e7101330) + * update dependency commander to v14.0.3 (#13946) (765c1291) + * update dependency pg-cursor to v2.17.0 (#13942) (1f285293) + * update dependency pg to v8.18.0 (#13941) (4eb8854c) + * update dependency autoprefixer to v10.4.24 (#13940) (b837c253) + * update dependency nodebb-theme-harmony to v2.1.37 (#13935) (9ec96aec) + * update dependency nodemailer to v7.0.13 (#13934) (290198b1) + * update dependency express-useragent to v2.1.0 (#13929) (82d6f35b) + * update dependency lru-cache to v11.2.5 (#13932) (b4c5657a) + * update dependency ace-builds to v1.43.6 (#13922) (d911a736) + * update dependency sass to v1.97.3 (#13925) (87c4d416) + * update dependency express-session to v1.19.0 (#13926) (e0e7c5ea) + * update dependency nodebb-theme-harmony to v2.1.36 (#13923) (abfb10e3) + * update dependency pg-cursor to v2.16.2 (#13915) (14e20a32) + * update dependency lodash to v4.17.23 (#13916) (0b822c96) + * update dependency pg to v8.17.2 (#13914) (3f50d52a) + * update dependency pg to v8.17.1 (#13901) (271239d4) + * update dependency pg to v8.17.1 (#13893) (cc8b2db5) + * update dependency pg-cursor to v2.16.1 (#13894) (62498a3c) + * update dependency nodebb-theme-harmony to v2.1.35 (#13896) (5b5955d6) + * update dependency satori to v0.19.1 (#13898) (dd6fda81) + +##### Other Changes + +* fix lint errors (fc474238) +* remove unused (68acc059) +* remove useless assignment (652629df) +* #13982, dont use btn-group on dropdowns (803473ca) +* //github.com/NodeBB/NodeBB/issues/13982 (26af029a) +* remove unused regex (1747cf81) +* filter at the end of user.search (dcbbc187) +* invalid-username doesnt have params (fb460725) + +##### Performance Improvements + +* don't call getUids on every topic load (8a60d9c2) + +##### Refactors + +* add topic-await-review notif text (bac9f1f7) +* ActivityPub ACP page to its own top-level section with sub-pages (f0d2be35) +* allow passing an array to topics.purge and topics.purgePost… (#14018) (e4c945f6) +* dont store ap pids in posts:pid (de1f04d9) +* closes #7155, (bb7be8c5) +* shorter check (a70a62cc) +* isHookValid (63c9a6e0) +* add icons to selected/all (970a4204) +* users table (e3d7abe0) +* pass options to thumbs.load (0424728f) +* slugify isn't heavy anymore (fb95f8a3) +* don't create giant array, process in batches of 500 (ed8cbd6e) +* use opendir instead of loading all files (ce9bd0bb) +* teaser object schema to its own file (1869b807) +* privileges.global.can works with array of privileges (fd6984d2) +* shorter hook (a8a85bcb) +* updateTags to modern js (3756a8fe) +* shorter check (b0f2fead) +* cache page to table (7336c58c) +* emoji replacement code into helper function, remove use of regex on untrusted user input (9608cce6) +* add createFieldChecker (#13973) (c65af199) +* run searches in parallel (9a198c38) +* shorter (3f67a000) +* remove unused url (7f4d537d) +* get rid of url.parse in core (157959df) +* use translator.compile which escapes % and , (a2f4c185) +* shorter (b3dc7f43) +* remove log (ff1376b3) +* get rid of cache for tid posters, was never cleared (065abbf2) +* dont use module, explodes on latest webpack (a4e3fe10) +* use lru directly (c8cd34bf) +* format (070d77fd) +* shorter tpl (e2fc349d) +* get rid of global.env, use process.env.NODE_ENV (07d1f224) +* make custom user field icons fixed width in the acp (2ded6813) +* use local cache for plugin isActive check (7ac5446a) +* Actors.getLocalFollowers to Actors.getFollowers, can pass in both local and remote ids (fac31859) +* move username check to createOrQueue (37675689) +* checkUsername function https://github.com/NodeBB/NodeBB/issues/10864 (7e27da61) +* shorter params (05e76edd) +* move ap jobs to its own file (c595edb4) + +##### Tests + +* remove assert (872d7c74) +* remove old test (59f35e6f) +* update tests to allow title-less topics (b1c097f8) +* show topic data on test fail (6e6900ba) +* fix redis, from was string in map, but int in notif object (a8c68ddc) +* fix spec (0e2a42d5) +* fix missing priv in tests (5c73d338) +* add test to check picture!=uploadedpicture (0c2ab232) +* fix hasOwn (d52b1359) +* group members test (ffc4c0dd) +* add more info to failiing response (87fdca2a) +* fix spec, remove log (05dd46c3) +* favicon test (ca237e67) +* dont return cover:url if its not requested (e4d852b4) +* fix username test (db07456b) +* fix test that explodes on new URL (694b545c) +* another test fix (94873c33) +* fix file name (05d4d857) +* fix spec (d601847a) +* fix tests (0d19294a) +* fix tests, update mentions composer (d6c69465) +* fix typo in spec (43be594a) +* fix spec (d1a39554) +* add missing spec for admin page (30014f41) +* remove unused (018e1c5f) +* fix spec (bc1593b2) + #### v4.8.1 (2026-01-28) ##### Chores diff --git a/install/package.json b/install/package.json index 091f652ba8..cde6256eb3 100644 --- a/install/package.json +++ b/install/package.json @@ -97,7 +97,7 @@ "multer": "2.0.2", "nconf": "0.13.0", "nodebb-plugin-2factor": "7.6.1", - "nodebb-plugin-composer-default": "10.3.22", + "nodebb-plugin-composer-default": "10.3.23", "nodebb-plugin-dbsearch": "6.4.0", "nodebb-plugin-emoji": "6.0.5", "nodebb-plugin-emoji-android": "4.1.1", @@ -107,7 +107,7 @@ "nodebb-plugin-spam-be-gone": "2.3.2", "nodebb-plugin-web-push": "0.7.6", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.2.42", + "nodebb-theme-harmony": "2.2.43", "nodebb-theme-lavender": "7.1.21", "nodebb-theme-peace": "2.2.55", "nodebb-theme-persona": "14.2.21", diff --git a/public/language/en-GB/admin/dashboard.json b/public/language/en-GB/admin/dashboard.json index a35775fdce..1cec2284e7 100644 --- a/public/language/en-GB/admin/dashboard.json +++ b/public/language/en-GB/admin/dashboard.json @@ -8,10 +8,11 @@ "topics": "Topics", "remote-posts": "Remote Posts", "remote-topics": "Remote Topics", + "messages": "Messages", "page-views-seven": "Last 7 Days", "page-views-thirty": "Last 30 Days", "page-views-last-day": "Last 24 hours", - "page-views-custom": "Custom Date Range", + "page-views-custom": "Custom Range", "page-views-custom-start": "Range Start", "page-views-custom-end": "Range End", "page-views-custom-help": "Enter a date range of page views you would like to view. If no date picker is available, the accepted format is YYYY-MM-DD", diff --git a/public/openapi/write/categories/cid/watch.yaml b/public/openapi/write/categories/cid/watch.yaml index 8ed5a10d1e..c46ceaaa2e 100644 --- a/public/openapi/write/categories/cid/watch.yaml +++ b/public/openapi/write/categories/cid/watch.yaml @@ -55,7 +55,7 @@ put: type: array description: A list of cids that have had their watch states modified. items: - type: number + type: string delete: tags: - categories @@ -99,4 +99,4 @@ delete: type: array description: A list of cids that have had their watch states modified. items: - type: number \ No newline at end of file + type: string \ No newline at end of file diff --git a/public/scss/admin/general/dashboard.scss b/public/scss/admin/general/dashboard.scss index 2cfc461a0e..50c9390072 100644 --- a/public/scss/admin/general/dashboard.scss +++ b/public/scss/admin/general/dashboard.scss @@ -1,20 +1,26 @@ +.template-admin-dashboard #analytics-panel .graph-container { + min-height: 300px; +} .dashboard { .card { max-width: 100% !important; } + #analytics-panel.fullscreen .graph-container { + width: 100%!important; + height: 100%!important; + max-height: 100%!important; + padding: 40px; + } + .graph-container { + max-height: 400px; position: relative; background: var(--bg-body-bg); &.pie-chart { max-height: 180px; } - - &.fullscreen { - width: 100%; - padding: 40px; - } } .graph-legend { diff --git a/public/src/admin/dashboard.js b/public/src/admin/dashboard.js index 8be06177b0..a290a791c7 100644 --- a/public/src/admin/dashboard.js +++ b/public/src/admin/dashboard.js @@ -18,6 +18,8 @@ import * as alerts from '../modules/alerts'; import * as translator from '../modules/translator'; import { formattedNumber } from '../modules/helpers'; +import { setupFullscreen } from './modules/fullscreen'; + Chart.register( LineController, DoughnutController, @@ -75,7 +77,7 @@ export function init() { socket.emit('admin.rooms.getAll', updateRoomUsage); initiateDashboard(); }); - setupFullscreen(); + setupFullscreen($('#expand-analytics'), $('#analytics-panel')); } function updateRoomUsage(err, data) { @@ -579,36 +581,3 @@ function initiateDashboard(realtime) { }, realtime ? DEFAULTS.realtimeInterval : DEFAULTS.graphInterval); } -function setupFullscreen() { - const container = document.getElementById('analytics-panel'); - const $container = $(container); - const btn = $container.find('#expand-analytics'); - let fsMethod; - let exitMethod; - - if (container.requestFullscreen) { - fsMethod = 'requestFullscreen'; - exitMethod = 'exitFullscreen'; - } else if (container.mozRequestFullScreen) { - fsMethod = 'mozRequestFullScreen'; - exitMethod = 'mozCancelFullScreen'; - } else if (container.webkitRequestFullscreen) { - fsMethod = 'webkitRequestFullscreen'; - exitMethod = 'webkitCancelFullScreen'; - } else if (container.msRequestFullscreen) { - fsMethod = 'msRequestFullscreen'; - exitMethod = 'msCancelFullScreen'; - } - - if (fsMethod) { - btn.on('click', function () { - if ($container.hasClass('fullscreen')) { - document[exitMethod](); - $container.removeClass('fullscreen'); - } else { - container[fsMethod](); - $container.addClass('fullscreen'); - } - }); - } -} diff --git a/public/src/admin/dashboard/logins.js b/public/src/admin/dashboard/logins.js index eea048293c..c82b54f17b 100644 --- a/public/src/admin/dashboard/logins.js +++ b/public/src/admin/dashboard/logins.js @@ -1,6 +1,6 @@ 'use strict'; -define('admin/dashboard/logins', ['admin/modules/dashboard-line-graph'], (graph) => { +define('admin/dashboard/logins', ['admin/modules/dashboard-line-graph', 'admin/modules/fullscreen'], (graph, { setupFullscreen }) => { const ACP = {}; ACP.init = () => { @@ -8,6 +8,7 @@ define('admin/dashboard/logins', ['admin/modules/dashboard-line-graph'], (graph) set: 'logins', dataset: ajaxify.data.dataset, }); + setupFullscreen($('#expand-analytics'), $('#analytics-panel')); }; return ACP; diff --git a/public/src/admin/dashboard/topics.js b/public/src/admin/dashboard/topics.js index 91e0754779..32e2bede3e 100644 --- a/public/src/admin/dashboard/topics.js +++ b/public/src/admin/dashboard/topics.js @@ -1,6 +1,8 @@ 'use strict'; -define('admin/dashboard/topics', ['admin/modules/dashboard-line-graph', 'hooks'], (graph, hooks) => { +define('admin/dashboard/topics', [ + 'admin/modules/dashboard-line-graph', 'hooks', 'admin/modules/fullscreen', +], (graph, hooks, { setupFullscreen }) => { const ACP = {}; ACP.init = () => { @@ -10,6 +12,7 @@ define('admin/dashboard/topics', ['admin/modules/dashboard-line-graph', 'hooks'] }).then(() => { hooks.onPage('action:admin.dashboard.updateGraph', ACP.updateTable); }); + setupFullscreen($('#expand-analytics'), $('#analytics-panel')); }; ACP.updateTable = () => { diff --git a/public/src/admin/dashboard/users.js b/public/src/admin/dashboard/users.js index c9798e9572..7ef00ea477 100644 --- a/public/src/admin/dashboard/users.js +++ b/public/src/admin/dashboard/users.js @@ -1,6 +1,8 @@ 'use strict'; -define('admin/dashboard/users', ['admin/modules/dashboard-line-graph', 'hooks'], (graph, hooks) => { +define('admin/dashboard/users', [ + 'admin/modules/dashboard-line-graph', 'hooks', 'admin/modules/fullscreen', +], (graph, hooks, { setupFullscreen }) => { const ACP = {}; ACP.init = () => { @@ -10,6 +12,7 @@ define('admin/dashboard/users', ['admin/modules/dashboard-line-graph', 'hooks'], }).then(() => { hooks.onPage('action:admin.dashboard.updateGraph', ACP.updateTable); }); + setupFullscreen($('#expand-analytics'), $('#analytics-panel')); }; ACP.updateTable = () => { diff --git a/public/src/admin/manage/categories.js b/public/src/admin/manage/categories.js index a8a6a2fb28..babdb83367 100644 --- a/public/src/admin/manage/categories.js +++ b/public/src/admin/manage/categories.js @@ -11,7 +11,7 @@ define('admin/manage/categories', [ ], function (translator, Benchpress, categorySelector, api, Sortable, bootbox, alerts) { Sortable = Sortable.default; const Categories = {}; - let newCategoryId = -1; + let newCategoryId = '-1'; let sortables; Categories.init = function () { @@ -261,15 +261,15 @@ define('admin/manage/categories', [ }; function itemDidAdd(e) { - newCategoryId = e.to.dataset.cid; + newCategoryId = String(e.to.dataset.cid); } function itemDragDidEnd(e) { - const isCategoryUpdate = parseInt(newCategoryId, 10) !== -1; + const isCategoryUpdate = String(newCategoryId) !== '-1'; // Update needed? if ((e.newIndex != null && parseInt(e.oldIndex, 10) !== parseInt(e.newIndex, 10)) || isCategoryUpdate) { - const cid = e.item.dataset.cid; + const cid = String(e.item.dataset.cid); const modified = {}; // on page 1 baseIndex is 0, on page n baseIndex is (n - 1) * ajaxify.data.categoriesPerPage // this makes sure order is correct when drag & drop is used on pages > 1 @@ -282,8 +282,8 @@ define('admin/manage/categories', [ modified[cid].parentCid = newCategoryId; // Show/hide expand buttons after drag completion - const oldParentCid = parseInt(e.from.getAttribute('data-cid'), 10); - const newParentCid = parseInt(e.to.getAttribute('data-cid'), 10); + const oldParentCid = String(e.from.getAttribute('data-cid') || ''); + const newParentCid = String(e.to.getAttribute('data-cid') || ''); if (oldParentCid !== newParentCid) { const toggle = document.querySelector(`.categories li[data-cid="${newParentCid}"] .toggle`); if (toggle) { diff --git a/public/src/admin/manage/category.js b/public/src/admin/manage/category.js index 7a6e85486e..d75318a4fd 100644 --- a/public/src/admin/manage/category.js +++ b/public/src/admin/manage/category.js @@ -160,7 +160,7 @@ define('admin/manage/category', [ label: '[[modules:bootbox.confirm]]', className: 'btn-primary', callback: function () { - if (!selectedCid || parseInt(selectedCid, 10) === parseInt(ajaxify.data.category.cid, 10)) { + if (!selectedCid || String(selectedCid) === String(ajaxify.data.category.cid)) { return; } diff --git a/public/src/admin/manage/group.js b/public/src/admin/manage/group.js index c856f0b487..fc68fc8f63 100644 --- a/public/src/admin/manage/group.js +++ b/public/src/admin/manage/group.js @@ -68,10 +68,14 @@ define('admin/manage/group', [ const cidSelector = categorySelector.init($('.member-post-cids-selector [component="category-selector"]'), { onSelect: function (selectedCategory) { - let cids = ($('#memberPostCids').val() || '').split(',').map(cid => parseInt(cid, 10)); - cids.push(selectedCategory.cid); - cids = cids.filter((cid, index, array) => array.indexOf(cid) === index); - $('#memberPostCids').val(cids.join(',')); + const cids = new Set(($('#memberPostCids').val() || '').split(',').filter(Boolean)); + if (cids.has(String(selectedCategory.cid))) { + cids.delete(String(selectedCategory.cid)); + } else { + cids.add(String(selectedCategory.cid)); + } + + $('#memberPostCids').val(Array.from(cids).join(',')); cidSelector.selectCategory(0); return false; }, diff --git a/public/src/admin/modules/dashboard-line-graph.js b/public/src/admin/modules/dashboard-line-graph.js index 3087518c9d..cbc2bcf5bf 100644 --- a/public/src/admin/modules/dashboard-line-graph.js +++ b/public/src/admin/modules/dashboard-line-graph.js @@ -72,13 +72,14 @@ export function init({ set, dataset }) { data: data, options: { responsive: true, + maintainAspectRatio: false, scales: { 'left-y-axis': { type: 'linear', position: 'left', beginAtZero: true, title: { - display: true, + display: false, text: key, }, }, diff --git a/public/src/admin/modules/fullscreen.js b/public/src/admin/modules/fullscreen.js new file mode 100644 index 0000000000..eac4ffd0c5 --- /dev/null +++ b/public/src/admin/modules/fullscreen.js @@ -0,0 +1,30 @@ +export function setupFullscreen($btn, $container) { + let fsMethod; + let exitMethod; + const container = $container.get(0); + if (container.requestFullscreen) { + fsMethod = 'requestFullscreen'; + exitMethod = 'exitFullscreen'; + } else if (container.mozRequestFullScreen) { + fsMethod = 'mozRequestFullScreen'; + exitMethod = 'mozCancelFullScreen'; + } else if (container.webkitRequestFullscreen) { + fsMethod = 'webkitRequestFullscreen'; + exitMethod = 'webkitCancelFullScreen'; + } else if (container.msRequestFullscreen) { + fsMethod = 'msRequestFullscreen'; + exitMethod = 'msCancelFullScreen'; + } + + if (fsMethod) { + $btn.on('click', function () { + if ($container.hasClass('fullscreen')) { + document[exitMethod]().catch(err => console.error(err)); + $container.removeClass('fullscreen'); + } else { + container[fsMethod]().catch(err => console.error(err)); + $container.addClass('fullscreen'); + } + }); + } +} \ No newline at end of file diff --git a/public/src/client/groups/details.js b/public/src/client/groups/details.js index 9ed57cd6ed..39395258cb 100644 --- a/public/src/client/groups/details.js +++ b/public/src/client/groups/details.js @@ -233,11 +233,16 @@ define('forum/groups/details', [ const cidSelector = categorySelector.init($('.member-post-cids-selector [component="category-selector"]'), { onSelect: function (selectedCategory) { - let cids = ($('#memberPostCids').val() || '').split(',').map(cid => parseInt(cid, 10)); - cids.push(selectedCategory.cid); - cids = cids.filter((cid, index, array) => array.indexOf(cid) === index); - $('#memberPostCids').val(cids.join(',')); + const cids = new Set(($('#memberPostCids').val() || '').split(',').filter(Boolean)); + if (cids.has(String(selectedCategory.cid))) { + cids.delete(String(selectedCategory.cid)); + } else { + cids.add(String(selectedCategory.cid)); + } + + $('#memberPostCids').val(Array.from(cids).join(',')); cidSelector.selectCategory(0); + return false; }, }); }; diff --git a/public/src/client/unread.js b/public/src/client/unread.js index 6851ba1ec4..d0b6428dc8 100644 --- a/public/src/client/unread.js +++ b/public/src/client/unread.js @@ -65,9 +65,7 @@ define('forum/unread', [ // Generate list of default categories based on topic list let defaultCategories = ajaxify.data.topics.reduce((map, topic) => { const { category } = topic; - let { cid } = category; - cid = utils.isNumber(cid) ? parseInt(cid, 10) : cid; - map.set(cid, category); + map.set(String(category.cid), category); return map; }, new Map()); defaultCategories = Array.from(defaultCategories.values()); diff --git a/public/src/modules/api.js b/public/src/modules/api.js index 55abd1ad36..7c1038fbde 100644 --- a/public/src/modules/api.js +++ b/public/src/modules/api.js @@ -79,7 +79,11 @@ async function xhr(options) { if (!res.ok) { if (response) { - throw new Error(isJSON ? response.status.message : response); + const jsonError = isJSON && (response.status?.message || response.error || ''); + throw new Error(isJSON && jsonError ? + jsonError : + response + ); } throw new Error(res.statusText); } diff --git a/public/src/modules/topicList.js b/public/src/modules/topicList.js index 4af7eee9c5..1d026c1719 100644 --- a/public/src/modules/topicList.js +++ b/public/src/modules/topicList.js @@ -95,11 +95,11 @@ define('topicList', [ const categories = d.selectedCids && d.selectedCids.length && - d.selectedCids.indexOf(parseInt(data.cid, 10)) === -1; + !d.selectedCids.includes(parseInt(data.cid, 10)); const filterWatched = d.selectedFilter && d.selectedFilter.filter === 'watched'; const category = d.template.category && - parseInt(d.cid, 10) !== parseInt(data.cid, 10); + String(d.cid) !== String(data.cid); const preventAlert = !!(categories || filterWatched || category || scheduledTopics.includes(data.tid)); hooks.fire('filter:topicList.onNewTopic', { topic: data, preventAlert }).then((result) => { @@ -126,14 +126,14 @@ define('topicList', [ const isMain = parseInt(post.topic.mainPid, 10) === parseInt(post.pid, 10); const categories = d.selectedCids && d.selectedCids.length && - d.selectedCids.indexOf(parseInt(post.topic.cid, 10)) === -1; + !d.selectedCids.includes(parseInt(post.topic.cid, 10)); const filterNew = d.selectedFilter && d.selectedFilter.filter === 'new'; const filterWatched = d.selectedFilter && d.selectedFilter.filter === 'watched' && !post.topic.isFollowing; const category = d.template.category && - parseInt(d.cid, 10) !== parseInt(post.topic.cid, 10); + String(d.cid) !== String(post.topic.cid); const preventAlert = !!(isMain || categories || filterNew || filterWatched || category); hooks.fire('filter:topicList.onNewPost', { post, preventAlert }).then((result) => { diff --git a/src/activitypub/helpers.js b/src/activitypub/helpers.js index 558a4ee8d7..044fc2c82d 100644 --- a/src/activitypub/helpers.js +++ b/src/activitypub/helpers.js @@ -559,6 +559,7 @@ Helpers.renderEmoji = (text, tags, strip = false) => { tags = Array.isArray(tags) ? tags : [tags]; let result = text; + const parsed = new Set(); tags.forEach((tag) => { const isEmoji = tag.type === 'Emoji'; const hasUrl = tag.icon && tag.icon.url; @@ -566,6 +567,9 @@ Helpers.renderEmoji = (text, tags, strip = false) => { if (isEmoji && (strip || (hasUrl && isImage))) { let { name } = tag; + if (parsed.has(name)) { + return; + } if (!name.startsWith(':')) { name = `:${name}`; @@ -583,6 +587,7 @@ Helpers.renderEmoji = (text, tags, strip = false) => { result = result.substring(0, index) + imgTag + result.substring(index + name.length); index = result.indexOf(name, index + imgTag.length); } + parsed.add(name); } }); diff --git a/src/api/categories.js b/src/api/categories.js index 4806868f93..d0dd8d47df 100644 --- a/src/api/categories.js +++ b/src/api/categories.js @@ -8,7 +8,6 @@ const user = require('../user'); const groups = require('../groups'); const privileges = require('../privileges'); const activitypub = require('../activitypub'); -const utils = require('../utils'); const categoriesAPI = module.exports; @@ -158,7 +157,7 @@ categoriesAPI.getTopics = async (caller, data) => { categoriesAPI.setWatchState = async (caller, { cid, state, uid }) => { let targetUid = caller.uid; let cids = Array.isArray(cid) ? cid : [cid]; - cids = cids.map(cid => (utils.isNumber(cid) ? parseInt(cid, 10) : cid)); + cids = cids.map(cid => String(cid)); if (uid) { targetUid = uid; @@ -170,9 +169,9 @@ categoriesAPI.setWatchState = async (caller, { cid, state, uid }) => { // filter to subcategories of cid let cat; do { - cat = categoryData.find(c => !cids.includes(c.cid) && cids.includes(c.parentCid)); + cat = categoryData.find(c => !cids.includes(String(c.cid)) && cids.includes(String(c.parentCid))); if (cat) { - cids.push(cat.cid); + cids.push(String(cat.cid)); } } while (cat); diff --git a/src/api/search.js b/src/api/search.js index 83db189de3..a5bc32f83a 100644 --- a/src/api/search.js +++ b/src/api/search.js @@ -22,7 +22,7 @@ searchApi.categories = async (caller, data) => { data.states = (data.states || ['watching', 'tracking', 'notwatching', 'ignoring']).map( state => categories.watchStates[state] ); - data.parentCid = parseInt(data.parentCid || 0, 10); + data.parentCid = String(data.parentCid || 0); let cids; if (data.search) { @@ -42,15 +42,15 @@ searchApi.categories = async (caller, data) => { }); if (Array.isArray(data.selectedCids)) { - data.selectedCids = data.selectedCids.map(cid => parseInt(cid, 10)); + data.selectedCids = data.selectedCids.map(cid => String(cid)); } let categoriesData = categories.buildForSelectCategories(visibleCategories, ['disabledClass'], data.parentCid); categoriesData = categoriesData.slice(0, 1000); categoriesData.forEach((category) => { - category.selected = data.selectedCids ? data.selectedCids.includes(category.cid) : false; - if (matchedCids.includes(category.cid)) { + category.selected = data.selectedCids ? data.selectedCids.includes(String(category.cid)) : false; + if (matchedCids.includes(String(category.cid))) { category.match = true; } }); @@ -72,7 +72,7 @@ async function findMatchedCids(uid, data) { localOnly: data.localOnly, }); - let matchedCids = result.categories.map(c => c.cid); + let matchedCids = result.categories.map(c => String(c.cid)); // no need to filter if all 3 states are used const filterByWatchState = !Object.values(categories.watchStates) .every(state => data.states.includes(state)); @@ -82,8 +82,12 @@ async function findMatchedCids(uid, data) { matchedCids = matchedCids.filter((cid, index) => data.states.includes(states[index])); } - const rootCids = _.uniq(_.flatten(await Promise.all(matchedCids.map(categories.getParentCids)))); - const allChildCids = _.uniq(_.flatten(await Promise.all(matchedCids.map(categories.getChildrenCids)))); + const rootCids = _.uniq(_.flatten( + await Promise.all(matchedCids.map(categories.getParentCids)) + )); + const allChildCids = _.uniq(_.flatten( + await Promise.all(matchedCids.map(categories.getChildrenCids)) + )); return { cids: _.uniq(rootCids.concat(allChildCids).concat(matchedCids)), @@ -145,7 +149,7 @@ searchApi.roomUsers = async (caller, { query, roomId }) => { roomUsers.forEach((user, index) => { if (user) { user.isOwner = isOwners[index]; - user.canKick = isRoomOwner && (parseInt(user.uid, 10) !== parseInt(caller.uid, 10)); + user.canKick = isRoomOwner && String(user.uid) !== String(caller.uid); } }); diff --git a/src/categories/create.js b/src/categories/create.js index 92f63f27e6..fa49468834 100644 --- a/src/categories/create.js +++ b/src/categories/create.js @@ -101,7 +101,7 @@ module.exports = function (Categories) { await privileges.categories.give(result.guestPrivileges, category.cid, ['guests', 'spiders']); cache.del('categories:cid'); - await clearParentCategoryCache(parentCid); + await Categories.clearParentCategoryCache(parentCid); if (data.cloneFromCid && parseInt(data.cloneFromCid, 10)) { category = await Categories.copySettingsFrom(data.cloneFromCid, category.cid, !data.parentCid); @@ -115,8 +115,8 @@ module.exports = function (Categories) { return category; }; - async function clearParentCategoryCache(parentCid) { - while (parseInt(parentCid, 10) >= 0) { + Categories.clearParentCategoryCache = async function (parentCid) { + while (parentCid || parseInt(parentCid, 10) === 0) { cache.del([ `cid:${parentCid}:children`, `cid:${parentCid}:children:all`, @@ -129,7 +129,7 @@ module.exports = function (Categories) { // eslint-disable-next-line no-await-in-loop parentCid = await Categories.getCategoryField(parentCid, 'parentCid'); } - } + }; async function duplicateCategoriesChildren(parentCid, cid, uid) { let children = await Categories.getChildren([cid], uid); @@ -188,17 +188,13 @@ module.exports = function (Categories) { throw new Error('[[error:invalid-cid]]'); } - const oldParent = parseInt(destination.parentCid, 10) || 0; - const newParent = parseInt(source.parentCid, 10) || 0; - if (copyParent && newParent !== parseInt(toCid, 10)) { + const oldParent = String(destination.parentCid || 0); + const newParent = String(source.parentCid || 0); + if (copyParent && newParent !== String(toCid)) { await db.sortedSetRemove(`cid:${oldParent}:children`, toCid); await db.sortedSetAdd(`cid:${newParent}:children`, source.order, toCid); - cache.del([ - `cid:${oldParent}:children`, - `cid:${oldParent}:children:all`, - `cid:${newParent}:children`, - `cid:${newParent}:children:all`, - ]); + await Categories.clearParentCategoryCache(oldParent); + await Categories.clearParentCategoryCache(newParent); } destination.description = source.description; diff --git a/src/categories/delete.js b/src/categories/delete.js index 3074cf3f55..5f96e32676 100644 --- a/src/categories/delete.js +++ b/src/categories/delete.js @@ -79,12 +79,13 @@ module.exports = function (Categories) { cache.del([ 'categories:cid', 'cid:0:children', - `cid:${parentCid}:children`, - `cid:${parentCid}:children:all`, - `cid:${cid}:children`, - `cid:${cid}:children:all`, + 'cid:0:children:all', `cid:${cid}:tag:whitelist`, ]); + await Promise.all([ + Categories.clearParentCategoryCache(parentCid), + Categories.clearParentCategoryCache(cid), + ]); } async function deleteTags(cid) { diff --git a/src/categories/index.js b/src/categories/index.js index 948ff86226..df66600794 100644 --- a/src/categories/index.js +++ b/src/categories/index.js @@ -104,7 +104,6 @@ Categories.getAllCidsFromSet = async function (key) { } cids = await db.getSortedSetRange(key, 0, -1); - cids = cids.map(cid => utils.isNumber(cid) ? parseInt(cid, 10) : cid); cache.set(key, cids); return cids.slice(); }; @@ -261,7 +260,7 @@ Categories.getChildren = async function (cids, uid) { async function getChildrenTree(category, uid) { let childrenCids = await Categories.getChildrenCids(category.cid); childrenCids = await privileges.categories.filterCids('find', childrenCids, uid); - childrenCids = childrenCids.filter(cid => parseInt(category.cid, 10) !== parseInt(cid, 10)); + childrenCids = childrenCids.filter(cid => String(category.cid) !== String(cid)); if (!childrenCids.length) { category.children = []; return; @@ -280,7 +279,7 @@ Categories.getParentCids = async function (currentCid) { // eslint-disable-next-line cid = await Categories.getCategoryField(cid, 'parentCid'); if (cid) { - parents.unshift(cid); + parents.unshift(String(cid)); } } return parents; @@ -291,12 +290,12 @@ Categories.getChildrenCids = async function (rootCid) { async function recursive(keys) { let childrenCids = await db.getSortedSetRange(keys, 0, -1); - childrenCids = childrenCids.filter(cid => !allCids.includes(utils.isNumber(cid) ? parseInt(cid, 10) : cid)); + childrenCids = childrenCids.filter(cid => !allCids.includes(cid)); if (!childrenCids.length) { return; } + allCids.push(...childrenCids); keys = childrenCids.map(cid => `cid:${cid}:children`); - childrenCids.forEach(cid => allCids.push(utils.isNumber(cid) ? parseInt(cid, 10) : cid)); await recursive(keys); } const key = `cid:${rootCid}:children`; @@ -331,7 +330,7 @@ Categories.flattenCategories = function (allCategories, categoryData) { * @param parentCid {number} start from 0 to build full tree */ Categories.getTree = function (categories, parentCid) { - parentCid = parentCid || 0; + parentCid = String(parentCid || 0); const cids = categories.map(category => category && category.cid); const cidToCategory = {}; const parents = {}; @@ -352,15 +351,15 @@ Categories.getTree = function (categories, parentCid) { return; } if (!category.hasOwnProperty('parentCid') || category.parentCid === null) { - category.parentCid = 0; + category.parentCid = '0'; } - if (category.parentCid === parentCid) { + if (String(category.parentCid) === parentCid) { tree.push(category); category.parent = parents[parentCid]; } else { - const parent = cidToCategory[category.parentCid]; + const parent = cidToCategory[String(category.parentCid)]; if (parent && parent.cid !== category.cid) { - category.parent = parents[category.parentCid]; + category.parent = parents[String(category.parentCid)]; parent.children = parent.children || []; parent.children.push(category); } @@ -414,10 +413,10 @@ Categories.buildForSelectCategories = function (categories, fields, parentCid) { category.children.forEach(child => recursive(child, categoriesData, `    ${level}`, depth + 1)); } } - parentCid = parentCid || 0; + parentCid = String(parentCid || 0); const categoriesData = []; - const rootCategories = categories.filter(category => category && category.parentCid === parentCid); + const rootCategories = categories.filter(category => category && String(category.parentCid) === parentCid); rootCategories.sort((a, b) => { if (a.order !== b.order) { diff --git a/src/categories/search.js b/src/categories/search.js index b0b8bef9b9..9f8c407e76 100644 --- a/src/categories/search.js +++ b/src/categories/search.js @@ -94,7 +94,7 @@ module.exports = function (Categories) { } async function getChildrenCids(cids, uid) { - const childrenCids = await Promise.all(cids.map(cid => Categories.getChildrenCids(cid))); + const childrenCids = await Promise.all(cids.map(Categories.getChildrenCids)); return await privileges.categories.filterCids('find', _.flatten(childrenCids), uid); } }; diff --git a/src/categories/update.js b/src/categories/update.js index ff4d6e4d11..2c2053f9fa 100644 --- a/src/categories/update.js +++ b/src/categories/update.js @@ -67,8 +67,8 @@ module.exports = function (Categories) { } async function updateParent(cid, newParent) { - newParent = parseInt(newParent, 10) || 0; - if (parseInt(cid, 10) === newParent) { + newParent = String(newParent || 0); + if (String(cid) === newParent) { throw new Error('[[error:cant-set-self-as-parent]]'); } const childrenCids = await Categories.getChildrenCids(cid); @@ -76,7 +76,7 @@ module.exports = function (Categories) { throw new Error('[[error:cant-set-child-as-parent]]'); } const categoryData = await Categories.getCategoryFields(cid, ['parentCid', 'order']); - const oldParent = categoryData.parentCid; + const oldParent = String(categoryData.parentCid); if (oldParent === newParent) { return; } @@ -85,12 +85,9 @@ module.exports = function (Categories) { db.sortedSetAdd(`cid:${newParent}:children`, categoryData.order, cid), db.setObjectField(`${utils.isNumber(cid) ? 'category' : 'categoryRemote'}:${cid}`, 'parentCid', newParent), ]); - - cache.del([ - `cid:${oldParent}:children`, - `cid:${newParent}:children`, - `cid:${oldParent}:children:all`, - `cid:${newParent}:children:all`, + await Promise.all([ + Categories.clearParentCategoryCache(oldParent), + Categories.clearParentCategoryCache(newParent), ]); } @@ -134,11 +131,9 @@ module.exports = function (Categories) { await db.setObjectBulk( childrenCids.map((cid, index) => [`${utils.isNumber(cid) ? 'category' : 'categoryRemote'}:${cid}`, { order: index + 1 }]) ); - + await Categories.clearParentCategoryCache(parentCid); cache.del([ 'categories:cid', - `cid:${parentCid}:children`, - `cid:${parentCid}:children:all`, ]); } diff --git a/src/controllers/admin/categories.js b/src/controllers/admin/categories.js index 5e503b1964..96fd2336c7 100644 --- a/src/controllers/admin/categories.js +++ b/src/controllers/admin/categories.js @@ -52,7 +52,7 @@ categoriesController.getAll = async function (req, res) { const rootCid = parseInt(req.query.cid, 10) || 0; const rootChildren = await categories.getAllCidsFromSet(`cid:${rootCid}:children`); async function getRootAndChildren() { - const childCids = _.flatten(await Promise.all(rootChildren.map(cid => categories.getChildrenCids(cid)))); + const childCids = _.flatten(await Promise.all(rootChildren.map(categories.getChildrenCids))); return [rootCid].concat(rootChildren.concat(childCids)); } @@ -202,13 +202,14 @@ categoriesController.addRemote = async function (req, res) { return res.sendStatus(404); } - const score = await db.sortedSetCard('cid:0:children'); - const order = score + 1; // order is 1-based lol + const lastItem = await db.getSortedSetRevRangeWithScores('cid:0:children', 0, 0); + const order = lastItem.length ? lastItem[0].score + 1 : 1; await Promise.all([ db.sortedSetAdd('cid:0:children', order, id), categories.setCategoryField(id, 'order', order), ]); cache.del('cid:0:children'); + cache.del('cid:0:children:all'); res.sendStatus(200); }; @@ -231,7 +232,7 @@ categoriesController.removeRemote = async function (req, res) { const parentCid = await categories.getCategoryField(req.params.cid, 'parentCid'); await db.sortedSetRemove(`cid:${parentCid || 0}:children`, req.params.cid); - cache.del(`cid:${parentCid || 0}:children`); - + await categories.clearParentCategoryCache(parentCid || 0); + await categories.setCategoryField(req.params.cid, 'parentCid', 0); res.sendStatus(200); }; diff --git a/src/controllers/admin/dashboard.js b/src/controllers/admin/dashboard.js index f573c5e677..0cfd5350c1 100644 --- a/src/controllers/admin/dashboard.js +++ b/src/controllers/admin/dashboard.js @@ -140,7 +140,8 @@ async function getStats() { getStatsForSet('topics:tid', 'topicCount'), meta.config.activitypubEnabled ? getStatsForSet('postsRemote:pid', '') : null, meta.config.activitypubEnabled ? getStatsForSet('topicsRemote:tid', '') : null, - ])).filter(Boolean); + getStatsForSet('messages:mid', 'messageCount'), + ])); results[0].name = '[[admin/dashboard:graphs.page-views]]'; results[1].name = '[[admin/dashboard:unique-visitors]]'; @@ -162,6 +163,8 @@ async function getStats() { if (results[7]) { results[7].name = '[[admin/dashboard:remote-topics]]'; } + results[8].name = '[[admin/dashboard:messages]]'; + results = results.filter(Boolean); ({ results } = await plugins.hooks.fire('filter:admin.getStats', { results, @@ -224,7 +227,7 @@ function calculateDeltas(results) { function increasePercent(last, now) { const percent = last ? (now - last) / last * 100 : 0; - return percent.toFixed(0); + return (percent > 0 ? `+` : '') + percent.toFixed(0); } results.yesterday -= results.today; results.dayIncrease = increasePercent(results.yesterday, results.today); diff --git a/src/controllers/search.js b/src/controllers/search.js index 440deb0fc5..3a2c5af54e 100644 --- a/src/controllers/search.js +++ b/src/controllers/search.js @@ -201,7 +201,7 @@ async function buildSelectedCategoryLabel(selectedCids) { label = `[[search:categories-x, ${selectedCids.length}]]`; } else if (selectedCids.length === 1 && selectedCids[0] === 'watched') { label = `[[search:categories-watched-categories]]`; - } else if (selectedCids.length === 1 && parseInt(selectedCids[0], 10)) { + } else if (selectedCids.length === 1 && selectedCids[0]) { const categoryData = await categories.getCategoryData(selectedCids[0]); if (categoryData && categoryData.name) { label = `[[search:categories-x, ${categoryData.name}]]`; diff --git a/src/messaging/rooms.js b/src/messaging/rooms.js index 83de1c07c7..4356775a47 100644 --- a/src/messaging/rooms.js +++ b/src/messaging/rooms.js @@ -110,7 +110,7 @@ module.exports = function (Messaging) { [`chat:room:${roomId}:uids:online`, now, uid], ...( isPublic ? - [`chat:room:${roomId}:owners`, now, uid] : + [[`chat:room:${roomId}:owners`, now, uid]] : [uid].concat(data.uids).map(uid => ([`chat:room:${roomId}:owners`, now, uid])) ), ]), diff --git a/src/posts/category.js b/src/posts/category.js index b87342a89f..7ce02bf490 100644 --- a/src/posts/category.js +++ b/src/posts/category.js @@ -40,7 +40,7 @@ module.exports = function (Posts) { }; async function filterPidsBySingleCid(pids, cid) { - const isMembers = await db.isSortedSetMembers(`cid:${parseInt(cid, 10)}:pids`, pids); + const isMembers = await db.isSortedSetMembers(`cid:${cid}:pids`, pids); return pids.filter((pid, index) => pid && isMembers[index]); } }; diff --git a/src/search.js b/src/search.js index e1a7fb4287..9993a8121d 100644 --- a/src/search.js +++ b/src/search.js @@ -412,8 +412,12 @@ async function getChildrenCids(data) { if (!data.searchChildren) { return []; } - const childrenCids = await Promise.all(data.categories.map(cid => categories.getChildrenCids(cid))); - return await privileges.categories.filterCids('find', _.uniq(_.flatten(childrenCids)), data.uid); + const childrenCids = await Promise.all(data.categories.map(categories.getChildrenCids)); + return await privileges.categories.filterCids( + 'find', + _.uniq(_.flatten(childrenCids)), + data.uid + ); } async function getSearchUids(data) { diff --git a/src/socket.io/admin/categories.js b/src/socket.io/admin/categories.js index 53c541598d..1406edb596 100644 --- a/src/socket.io/admin/categories.js +++ b/src/socket.io/admin/categories.js @@ -36,7 +36,7 @@ Categories.copyPrivilegesFrom = async function (socket, data) { Categories.copyPrivilegesToAllCategories = async function (socket, data) { let cids = await categories.getAllCidsFromSet('categories:cid'); - cids = cids.filter(cid => parseInt(cid, 10) !== parseInt(data.cid, 10)); + cids = cids.filter(cid => String(cid) !== String(data.cid)); for (const toCid of cids) { // eslint-disable-next-line no-await-in-loop await categories.copyPrivilegesFrom(data.cid, toCid, data.group, data.filter); diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js index ae2378d73a..68e9f1942b 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -111,7 +111,9 @@ SocketCategories.ignore = async function (socket, data) { async function ignoreOrWatch(fn, socket, data) { let targetUid = socket.uid; - const cids = Array.isArray(data.cid) ? data.cid.map(cid => parseInt(cid, 10)) : [parseInt(data.cid, 10)]; + const cids = Array.isArray(data.cid) ? + data.cid.map(cid => String(cid)) : + [String(data.cid)]; if (data.hasOwnProperty('uid')) { targetUid = data.uid; } @@ -122,9 +124,9 @@ async function ignoreOrWatch(fn, socket, data) { // filter to subcategories of cid let cat; do { - cat = categoryData.find(c => !cids.includes(c.cid) && cids.includes(c.parentCid)); + cat = categoryData.find(c => !cids.includes(String(c.cid)) && cids.includes(String(c.parentCid))); if (cat) { - cids.push(cat.cid); + cids.push(String(cat.cid)); } } while (cat); diff --git a/src/topics/teaser.js b/src/topics/teaser.js index 03ea62b5c0..1ee9f0d437 100644 --- a/src/topics/teaser.js +++ b/src/topics/teaser.js @@ -17,9 +17,11 @@ module.exports = function (Topics) { } let uid = options; let { teaserPost } = meta.config; + let teaserParseType = 'plaintext'; if (typeof options === 'object') { uid = options.uid; teaserPost = options.teaserPost || meta.config.teaserPost; + teaserParseType = options.teaserParseType || 'plaintext'; } const counts = []; @@ -68,7 +70,7 @@ module.exports = function (Topics) { post.timestampISO = utils.toISOString(post.timestamp); tidToPost[post.tid] = post; }); - await Promise.all(postData.map(p => posts.parsePost(p, 'plaintext'))); + await Promise.all(postData.map(p => posts.parsePost(p, teaserParseType))); const teasers = topics.map((topic, index) => { if (!topic) { diff --git a/src/views/admin/dashboard/logins.tpl b/src/views/admin/dashboard/logins.tpl index 552ded83e1..814b10156d 100644 --- a/src/views/admin/dashboard/logins.tpl +++ b/src/views/admin/dashboard/logins.tpl @@ -23,7 +23,7 @@ {./user.username} {function.userAgentIcons} {../browser} {../version} on {../platform} - + {{{ end }}} diff --git a/src/views/admin/dashboard/searches.tpl b/src/views/admin/dashboard/searches.tpl index ef20b9223b..52d1b8ea20 100644 --- a/src/views/admin/dashboard/searches.tpl +++ b/src/views/admin/dashboard/searches.tpl @@ -1,6 +1,6 @@
-
-
+
+
@@ -14,7 +14,7 @@
- +
diff --git a/src/views/admin/dashboard/topics.tpl b/src/views/admin/dashboard/topics.tpl index 5769f342fe..ee92cd254a 100644 --- a/src/views/admin/dashboard/topics.tpl +++ b/src/views/admin/dashboard/topics.tpl @@ -14,7 +14,7 @@ - + {{{ end }}} diff --git a/src/views/admin/dashboard/users.tpl b/src/views/admin/dashboard/users.tpl index 4fbd026ef3..1db7e85ef7 100644 --- a/src/views/admin/dashboard/users.tpl +++ b/src/views/admin/dashboard/users.tpl @@ -22,7 +22,7 @@ - + {{{ end }}} diff --git a/src/views/admin/partials/dashboard/graph.tpl b/src/views/admin/partials/dashboard/graph.tpl index e6eb91ff29..c01b923109 100644 --- a/src/views/admin/partials/dashboard/graph.tpl +++ b/src/views/admin/partials/dashboard/graph.tpl @@ -3,21 +3,21 @@
{graphTitle} -
+
+ + - -
-
+
diff --git a/src/views/admin/partials/dashboard/stats.tpl b/src/views/admin/partials/dashboard/stats.tpl index 43c1d661a3..098dacddff 100644 --- a/src/views/admin/partials/dashboard/stats.tpl +++ b/src/views/admin/partials/dashboard/stats.tpl @@ -20,14 +20,12 @@
{{{ each stats }}} - diff --git a/test/messaging.js b/test/messaging.js index 4e2e709007..bbe310e27d 100644 --- a/test/messaging.js +++ b/test/messaging.js @@ -599,6 +599,20 @@ describe('Messaging Library', () => { const { roomId } = await api.users.getPrivateRoomId({ uid: mocks.users.foo.uid }, { uid: mocks.users.herp.uid }); assert(roomId); }); + + it('should create a public chat room', async () => { + const data = await api.chats.create({ + uid: mocks.users.foo.uid, + session: {}, + }, { + name: 'public room', + type: 'public', + uids: [], + groups: ['registered-users'], + }); + assert(data.roomId); + assert.strictEqual(data.public, true); + }); }); describe('toMid', () => { diff --git a/test/search.js b/test/search.js index f0e285cb9d..e0b1421786 100644 --- a/test/search.js +++ b/test/search.js @@ -130,9 +130,10 @@ describe('Search', () => { it('should search for categories', async () => { const socketCategories = require('../src/socket.io/categories'); - let data = await socketCategories.categorySearch({ uid: phoebeUid }, { query: 'baz', parentCid: 0 }); + let data = await socketCategories.categorySearch({ uid: phoebeUid }, { search: 'baz', parentCid: 0 }); + assert.strictEqual(data.length, 1); assert.strictEqual(data[0].name, 'baz category'); - data = await socketCategories.categorySearch({ uid: phoebeUid }, { query: '', parentCid: 0 }); + data = await socketCategories.categorySearch({ uid: phoebeUid }, { search: '', parentCid: 0 }); assert.strictEqual(data.length, 5); });
{../title} [[topic:posted-by, {../user.username}]]
{../uid} {../username} {../email}
- - {{{ if ./href }}} - {./name} - {{{ else }}} - {./name} - {{{ end }}} - + + {{{ if ./href }}} + {./name} + {{{ else }}} + {./name} + {{{ end }}} {formattedNumber(./yesterday)} {formattedNumber(./today)}