mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-03-23 04:40:36 +01:00
Merge commit '2d49da78897764f2901039d6324b4457d3ffeb3e' into v4.x
This commit is contained in:
411
CHANGELOG.md
411
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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 <code>YYYY-MM-DD</code>",
|
||||
|
||||
@@ -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
|
||||
type: string
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
30
public/src/admin/modules/fullscreen.js
Normal file
30
public/src/admin/modules/fullscreen.js
Normal file
@@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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`,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}]]`;
|
||||
|
||||
@@ -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]))
|
||||
),
|
||||
]),
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<a href="{config.relative_path}/uid/{./user.uid}">{./user.username}</a>
|
||||
{function.userAgentIcons} {../browser} {../version} on {../platform}
|
||||
</td>
|
||||
<td><span class="timeago" title="{./datetimeISO}"></span></td>
|
||||
<td class="text-nowrap"><span class="timeago" title="{./datetimeISO}"></span></td>
|
||||
</tr>
|
||||
{{{ end }}}
|
||||
</tbody>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="row dashboard px-lg-4">
|
||||
<div class="col-8 mx-auto">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="col-12 col-md-8 mx-auto">
|
||||
<div class="d-flex flex-wrap gap-3 justify-content-between align-items-center mb-3">
|
||||
<form class="d-flex flex-wrap gap-3 align-sm-items-center" method="GET">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<label class="form-label mb-0" for="start">[[admin/dashboard:start]]</label>
|
||||
@@ -14,7 +14,7 @@
|
||||
<button onclick="$('form').submit();return false;"class="btn btn-primary btn-sm" type="submit">[[admin/dashboard:filter]]</button>
|
||||
</div>
|
||||
</form>
|
||||
<button id="clear-search-history" class="btn btn-sm btn-light"><i class="fa fa-trash text-danger"></i> [[admin/dashboard:clear-search-history]]</button>
|
||||
<button id="clear-search-history" class="btn btn-sm btn-light text-nowrap"><i class="fa fa-trash text-danger"></i> [[admin/dashboard:clear-search-history]]</button>
|
||||
</div>
|
||||
|
||||
<table class="table table-sm text-sm search-list w-100">
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<tr>
|
||||
<td><a href="{config.relative_path}/topic/{../slug}">{../title}</a></td>
|
||||
<td>[[topic:posted-by, {../user.username}]]</td>
|
||||
<td><span class="timeago" data-title="{../timestampISO}"></span></td>
|
||||
<td class="text-nowrap"><span class="timeago" data-title="{../timestampISO}"></span></td>
|
||||
</tr>
|
||||
{{{ end }}}
|
||||
</tbody>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<td>{../uid}</td>
|
||||
<td>{../username}</td>
|
||||
<td>{../email}</td>
|
||||
<td><span class="timeago" title="{../joindateISO}"></span></td>
|
||||
<td class="text-nowrap"><span class="timeago" title="{../joindateISO}"></span></td>
|
||||
</tr>
|
||||
{{{ end }}}
|
||||
</tbody>
|
||||
|
||||
@@ -3,21 +3,21 @@
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
{graphTitle}
|
||||
|
||||
<div class="d-flex gap-1">
|
||||
<div class="d-flex gap-1 align-items-center">
|
||||
<a class="btn btn-sm btn-light lh-sm" target="_blank" id="view-as-json" href="{config.relative_path}/api/v3/admin/analytics/{set}?type=hourly" data-bs-toggle="tooltip" data-bs-placement="bottom" title="[[admin/dashboard:view-as-json]]"><i class="fa fa-fw fa-xs fa-terminal text-primary"></i></a>
|
||||
<a class="btn btn-sm btn-light lh-sm" id="expand-analytics" href="#" data-bs-toggle="tooltip" data-bs-placement="bottom" title="[[admin/dashboard:expand-analytics]]"><i class="fa fa-fw fa-xs fa-expand text-primary"></i></a>
|
||||
<select data-action="updateGraph" class="form-select form-select-sm">
|
||||
<option value="1">[[admin/dashboard:page-views-last-day]]</option>
|
||||
<option value="7">[[admin/dashboard:page-views-seven]]</option>
|
||||
<option value="30">[[admin/dashboard:page-views-thirty]]</option>
|
||||
<option value="custom">[[admin/dashboard:page-views-custom]]</option>
|
||||
<option value="range" class="hidden">[[admin/dashboard:page-views-custom]]</option>
|
||||
<option value="range" class="hidden"></option>
|
||||
</select>
|
||||
<a class="btn btn-sm btn-light lh-sm" target="_blank" id="view-as-json" href="{config.relative_path}/api/v3/admin/analytics/{set}?type=hourly" data-bs-toggle="tooltip" data-bs-placement="bottom" title="[[admin/dashboard:view-as-json]]"><i class="fa fa-fw fa-xs fa-terminal text-primary"></i></a>
|
||||
<a class="btn btn-sm btn-light lh-sm" id="expand-analytics" href="#" data-bs-toggle="tooltip" data-bs-placement="bottom" title="[[admin/dashboard:expand-analytics]]"><i class="fa fa-fw fa-xs fa-expand text-primary"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="graph-container position-relative" id="analytics-traffic-container" style="width: 100%; {{{ if template.admin/dashboard }}}min-height: 300px;{{{ end }}}">
|
||||
<div class="graph-container position-relative {template.name}" id="analytics-traffic-container" style="width: 100%;">
|
||||
<canvas id="analytics-traffic"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,14 +20,12 @@
|
||||
<tbody class="text-sm text-tabular">
|
||||
{{{ each stats }}}
|
||||
<tr>
|
||||
<td class="fw-bold text-nowrap">
|
||||
|
||||
{{{ if ./href }}}
|
||||
<a href="{./href}">{./name}</a>
|
||||
{{{ else }}}
|
||||
{./name}
|
||||
{{{ end }}}
|
||||
|
||||
<td class="fw-semibold text-nowrap">
|
||||
{{{ if ./href }}}
|
||||
<a href="{./href}">{./name}</a>
|
||||
{{{ else }}}
|
||||
{./name}
|
||||
{{{ end }}}
|
||||
</td>
|
||||
<td class="text-end">{formattedNumber(./yesterday)}</td>
|
||||
<td class="text-end">{formattedNumber(./today)}</td>
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user