diff --git a/.github/workflows/VM Deploy.yml b/.github/workflows/VM Deploy.yml new file mode 100644 index 0000000000..06247d08e0 --- /dev/null +++ b/.github/workflows/VM Deploy.yml @@ -0,0 +1,36 @@ +name: VM Deploy NodeBB + +on: + push: + branches: + - main + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true + +jobs: + build-and-deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js version + uses: actions/setup-node@v3 + with: + node-version: '20.17.0' + + - name: Deploy via SSH + uses: appleboy/ssh-action@v1.2.1 + with: + host: ${{ secrets.SERVER_IP }} + username: ${{ secrets.SERVER_ADMIN }} + password: ${{ secrets.ADMIN_PASSWORD }} + script: | + cd ${{ secrets.REPO_NAME }} + git pull origin main + docker compose down + docker compose -f docker-compose-redis.yml up --build -d diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index dd3e5e00d6..85babeee99 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -85,6 +85,12 @@ jobs: - name: Node tests run: npm test + - name: Install retire.js + run: npm install -g retire + + - name: Run retire.js + run: retire + - name: Extract coverage info run: npm run coverage diff --git a/.gitignore b/.gitignore index 17bbb8e5f5..b9ed31e2d8 100644 --- a/.gitignore +++ b/.gitignore @@ -78,3 +78,6 @@ test.sh dump.rdb .archiver_shadow/ .snapshots/ + +# stryker temp files +.stryker-tmp diff --git a/TOPIC_DISPLAY_ARCHITECTURE.md b/TOPIC_DISPLAY_ARCHITECTURE.md new file mode 100644 index 0000000000..7a78c0b6c8 --- /dev/null +++ b/TOPIC_DISPLAY_ARCHITECTURE.md @@ -0,0 +1,249 @@ +# Topic Display Architecture - Complete Tracing Guide + +## Overview +When a user views a topic/post in NodeBB, multiple files work together to fetch, process, and display all the topic information (title, content, tags, posting user, etc.). + +--- + +## Primary Files in the Display Flow + +### 1. **Main Controller: `/src/controllers/topics.js`** ⭐ **PRIMARY FILE** + +**This is the MAIN file responsible for displaying a topic with all information.** + +**Key Function:** `topicsController.get()` (lines 25-157) + +**What it does:** +- Receives HTTP GET request for a topic (line 25) +- Fetches basic topic data using `topics.getTopicData(tid)` (line 33) +- Gets user privileges and permissions (lines 37-43) +- Fetches all posts in the topic using `topics.getTopicWithPosts()` (line 85) +- Modifies posts based on user privileges (line 87) +- Builds breadcrumbs navigation (lines 176-184) +- Adds tags to the topic data (lines 199-274) +- Fetches topic author information (line 129) +- Gets crossposts (line 131) +- Creates pagination data (line 133-138) +- Prepares metadata tags (SEO, OpenGraph) (lines 193-272) +- **Finally renders the template with all data** (line 157): + ```javascript + res.render('topic', topicData); + ``` + +**All Available Variables in `topicData`:** +- `tid` - Topic ID +- `cid` - Category ID +- `title` - Topic title (escaped) +- `titleRaw` - Topic title (raw) +- `content` - Main post content +- `posts` - Array of all posts in the topic + - Each post contains: + - `pid` - Post ID + - `uid` - User ID + - `content` - Post content + - `timestamp` - Post creation time + - `edited` - Last edit time + - `user` - User object with username, userslug, picture, reputation, etc. + - `index` - Post index/position + - `votes` - Vote count + - `replies` - Nested replies data +- `uid` - Topic creator ID +- `author` - Topic creator object (username, userslug) +- `category` - Category object (name, icon, slug, cid) +- `tags` - Array of tags +- `thumbs` - Array of thumbnail images +- `postcount` - Total number of posts +- `lastposttime` - Timestamp of last post +- `lastposter` - Last poster information +- `upvotes` - Total upvotes on topic +- `downvotes` - Total downvotes +- `views` - View count +- `privileges` - User privileges object +- `pagination` - Pagination data +- `breadcrumbs` - Navigation breadcrumbs +- And many more metadata fields... + +--- + +### 2. **Topic Data Module: `/src/topics/index.js`** + +**Provides data fetching functions used by the controller.** + +**Key Functions:** +- `Topics.getTopicData(tid)` - Fetches single topic metadata + - Returns: topic ID, title, category, creator info, timestamps, etc. + +- `Topics.getTopicWithPosts(topicData, set, uid, start, stop, reverse)` (line 158) + - **Fetches all posts for a topic with complete data** + - Calls `Topics.getTopicPosts()` to get posts array + - Each post includes: content, user data, timestamp, votes, etc. + - Returns enriched topicData with `posts` array attached + +- `Topics.getTopicsData(tids)` - Gets metadata for multiple topics + +- `Topics.addPostData(postData, uid)` - Enriches post data with user info + +--- + +### 3. **Topic Posts Module: `/src/topics/posts.js`** + +**Handles fetching and processing posts within a topic.** + +**Key Functions:** +- `Topics.getTopicPosts(topicData, set, start, stop, uid, reverse)` (line 22) + - Gets posts from a specific range/index + - Includes main post and nested replies + - Filters based on user privileges + - Enriches with user data, timestamps, content + +- `Topics.getTopicWithPosts()` - Main function that orchestrates the post fetching + - Uses `posts.getPostsByPids()` to fetch post content + - Enumerates events (edits, moves, etc.) + +--- + +### 4. **Posts Module: `/src/posts/index.js`** + +**Low-level post data retrieval.** + +**Key Functions:** +- `Posts.getPostsByPids(pids, uid)` - Gets post content by post IDs + - Returns: pid, uid, content, timestamp, edited, votes, etc. + +- `Posts.getPostsFields(pids, fields)` - Gets specific fields from posts + +--- + +### 5. **Main Template: `/vendor/nodebb-theme-harmony-main/templates/topic.tpl`** ⭐ **PRIMARY TEMPLATE** + +**This is the main HTML template that renders all topic information.** + +**What it displays:** +```html + + + + +

{title}

+ + + + + + + + +
+ +
+ + +
+ +
+ + + + + + + + +``` + +--- + +### 6. **Supporting Partial Templates:** + +- `/src/views/partials/topic/post.tpl` - Renders individual post with user info +- `/src/views/partials/topic/tags.tpl` - Renders topic tags +- `/src/views/partials/topic/stats.tpl` - Renders topic statistics (replies, views) +- `/src/views/partials/topic/thumbs.tpl` - Renders thumbnail images +- `/src/views/partials/data/topic.tpl` - Post data attributes (line 1 shows data structure) + +--- + +## Data Flow Diagram + +``` +HTTP GET /topic/123/slug + ↓ +/src/controllers/topics.js::getTopic() + ↓ +topics.getTopicData(tid) → Fetch topic metadata + ↓ +topics.getTopicWithPosts() → Fetch posts array + ↓ +/src/topics/index.js::getTopicWithPosts() + ↓ +Topics.getTopicPosts() → Get posts from set + ↓ +/src/topics/posts.js::getTopicPosts() + ↓ +posts.getPostsByPids(pids, uid) → Get post content + ↓ +user.getUsersFields() → Get user info for each post + ↓ +Enrich with: votes, edits, timestamps, privileges, etc. + ↓ +Build topicData object with all fields (title, posts, author, category, tags, etc.) + ↓ +res.render('topic', topicData) + ↓ +/vendor/nodebb-theme-harmony-main/templates/topic.tpl + ↓ +HTML rendered with all topic information displayed +``` + +--- + +## Variable Mapping: From Controller to Template + +### Controller Variables → Template Usage + +| Controller Variable | Template Variable | Template File | +|---|---|---| +| `tid` | `{tid}` | topic.tpl | +| `title` | `{title}` | topic.tpl | +| `titleRaw` | `{titleRaw}` | For SEO | +| `posts[]` | `{posts}` | topic.tpl (each loop) | +| `posts[].content` | `{posts.content}` | partials/topic/post.tpl | +| `posts[].user` | `{posts.user}` | Shows username, avatar, reputation | +| `posts[].timestamp` | `{posts.timestamp}` | Shows post time | +| `posts[].index` | `{posts.index}` | Post position tracking | +| `category` | `{category}` | Topic header - shows category badge | +| `tags` | `{tags}` | partials/topic/tags.tpl | +| `author` | `{author}` | Topic creator info | +| `upvotes` | `{upvotes}` | Statistics display | +| `postcount` | `{postcount}` | Statistics - total replies | +| `thumbs` | `{thumbs}` | partials/topic/thumbs.tpl | +| `privileges` | `{privileges}` | Controls edit/delete buttons visibility | + +--- + +## Summary + +**To trace any topic display variable:** + +1. **START**: `/src/controllers/topics.js` - Line 25 `getTopic()` function +2. **DATA FETCH**: Uses functions from `/src/topics/index.js` and `/src/topics/posts.js` +3. **BUILD DATA**: Assembles `topicData` object with all fields +4. **RENDER**: Passes `topicData` to template engine +5. **DISPLAY**: Template `/vendor/nodebb-theme-harmony-main/templates/topic.tpl` renders HTML + +All variable names are consistent between controller and template, making it easy to trace any field back to its source. + diff --git a/UserGuide.md b/UserGuide.md new file mode 100644 index 0000000000..b36eeaf8f9 --- /dev/null +++ b/UserGuide.md @@ -0,0 +1,136 @@ +# Features + +## Forwarding Posts + +### Purpose +To be able to forward/reference another post as part of a reply to a different topic. Allows for the connection of replies and ideas between topics. + +### Usage +In the Harmony theme, each post has a "Forward post" icon. Pressing this icon will open the reply text-editor with the forwarded post content pre-filled. Along with this text editor there will be a search bar that allows users to search which topic they want to forward the post to. Users can add their own additional text in the text editor along with the forwarded pre-filled content. + +### Testing +Step 1: Press "Forward Post" on the post you want to forward. +![img1](https://github.com/user-attachments/assets/c50654c8-e5ae-4414-9cbc-fa6fb89ae487)

+Step 2: The reply text-editor will appear. Choose a topic to forward to: +![img2](https://github.com/user-attachments/assets/dadd4567-74be-4e82-82e4-208bcf875560)

+Step 3: Add additional content to the post and press submit
+Step 4: View the forwarded post +![img3](https://github.com/user-attachments/assets/6f5d6260-15fc-4a07-8e81-00e5c52e4dd9) + +### Automated Testing +Automated tests can be found in test/posts.js on lines 305-349. The test creates two topics: one to forward to and one to forward from. It creates a reply with the forwarded header and additional content, then retrieves the reply and ascertains that the retrieved reply does indeed contain the forwarded post.
+Another test ensures that users cannot forward posts to their own topic. This test creates a post in a new topic, and uses the same comparison logic as the implementation code to check if the destination and source topics are identical. If so, it checks if an error was thrown.
+This feature is highly dependent on manual user testing to check if the flow of the feauture ("Forward post"-> choose topic -> add content -> view forwarded reply) is correct. Therefore the automated test only checks whether or not the reply does indeed contain the forwarded content in the case that a post was forwarded, and the case that an error is thrown. + + +## Verifiable Posts/Answers + +### Usage +In the Harmony theme, each post has a "Forward post" icon. Pressing this icon will open the reply text-editor with the forwarded post content pre-filled. Along with this text editor there will be a search bar that allows users to search which topic they want to forward the post to. Users can add their own additional text in the text editor along with the forwarded pre-filled content. + +### Testing +Step 1: Press "Forward Post" on the post you want to forward. +![img1](https://github.com/user-attachments/assets/c50654c8-e5ae-4414-9cbc-fa6fb89ae487)

+Step 2: The reply text-editor will appear. Choose a topic to forward to: +![img2](https://github.com/user-attachments/assets/dadd4567-74be-4e82-82e4-208bcf875560)

+Step 3: Add additional content to the post and press submit
+Step 4: View the forwarded post +![img3](https://github.com/user-attachments/assets/6f5d6260-15fc-4a07-8e81-00e5c52e4dd9) + +### Automated Testing +Automated tests can be found in test/posts.js on lines 305-349. The test creates two topics: one to forward to and one to forward from. It creates a reply with the forwarded header and additional content, then retrieves the reply and ascertains that the retrieved reply does indeed contain the forwarded post.
+Another test ensures that users cannot forward posts to their own topic. This test creates a post in a new topic, and uses the same comparison logic as the implementation code to check if the destination and source topics are identical. If so, it checks if an error was thrown.
+This feature is highly dependent on manual user testing to check if the flow of the feauture ("Forward post"-> choose topic -> add content -> view forwarded reply) is correct. Therefore the automated test only checks whether or not the reply does indeed contain the forwarded content in the case that a post was forwarded, and the case that an error is thrown. + +## Instructor-Only & Anonymous Posting +### Overview +Students may need to ask instructors questions that require sharing parts of their buggy code. In many CS courses, sharing code publicly (even unintentionally) can result in an Academic Integrity Violation (AIV).
+This feature introduces flexible post visibility options to ensure students can:
+* Ask questions publicly +* Ask anonymously +* Share posts visible only to instructors + +### Posting Options +When creating a post (via New Topic or Quick Reply), users will now see a Visibility dropdown instead of the previous anonymous toggle. + + +### Visibility Modes +The dropdown includes three options: +1) Post Publicly (default) + * Visible to all users who can access the topic + * Author identity is shown normally +2) Post Anonymously + * Visible to all users + * Author identity is masked + * Fully compatible with the existing anonymous system +3) Post to Instructors + * Visible only to: + * The post/topic author + * Administrators (instructors/moderators) + * Hidden from other students + +The selected option is stored as:
+visibilityMode = public | anonymous | instructors + +### How Instructor-Only Posts Work +Instructor-only posts are restricted at the read level.
+Access is granted only if: +* You are the author of the post, OR +* You are an admin/moderator (instructor) + + +All other users: +* Cannot see the post +* Cannot access its raw content +* Will not see it in topic listings + + +Instructor-only topics are also filtered out for non-author, non-admin users. + +### Anonymous Compatibility +This update preserves full backward compatibility with the existing anonymous posting system: +* The legacy “anonymous” flag still exists +* visibilityMode = anonymous maps internally to the anonymous behavior +* Author masking logic remains unchanged +* Composer logic includes a compatibility fallback to prevent regressions + + +### Where This Applies +The visibility dropdown appears in: +* New Topic Composer +* Quick Reply Composer + + +Both composers use the same visibility system. + +### Testing Instructions +Build and Run
+./nodebb build
+./nodebb restart
+Run full test suite:
+npm test
+ +### Manual Verification +1) Open Quick Reply and confirm the dropdown appears. +2) Confirm default selection is Post Publicly. +3) Create: + * One Public post + * One Anonymous post + * One Instructor-only post +4) Verify: + * Anonymous posts mask identity + * Instructor-only posts are visible only to author + admin + * Regular users cannot see instructor-only posts + +## Sorting Posts by Heat +Users can now navigate to a category and select "hot" from the sorting dropdown to sort topics by heat which is a function of activity and time + +Heat = (views × 1) + (posts × 5) + (upvotes × 20) + (age_decay × 100) + +Where: +age_decay = max(0.5, 1 - age_hours / 168) +age_hours = (current_time - last_post_time) / 3600000 + +Automated tests can be found in test/topics/heat.js which tests for accurate heat score calculation, proper handling of topics with zero, null, or high engagement, prioritizing different modes of activity (upvotes, posts, views), use of age decay, and correct sort order. + +## Instructor-only Posts \ No newline at end of file diff --git a/docker-compose-redis.yml b/docker-compose-redis.yml index 6334267ff7..755744d962 100644 --- a/docker-compose-redis.yml +++ b/docker-compose-redis.yml @@ -2,6 +2,7 @@ version: '3.8' services: nodebb: + user: "0" build: . # image: ghcr.io/nodebb/nodebb:latest restart: unless-stopped diff --git a/eslint.config.mjs b/eslint.config.mjs index 47cfa158f5..665ddccf73 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -29,6 +29,7 @@ export default defineConfig([ 'test/files/', '*.min.js', 'install/docker/', + '.archiver_shadow/', ], }, // tests diff --git a/index.htlp b/index.htlp new file mode 100644 index 0000000000..e69de29bb2 diff --git a/install/package.json b/install/package.json index 18929861ba..c134fb0750 100644 --- a/install/package.json +++ b/install/package.json @@ -98,7 +98,7 @@ "multer": "2.0.2", "nconf": "0.13.0", "nodebb-plugin-2factor": "7.6.1", - "nodebb-plugin-composer-default": "10.3.1", + "nodebb-plugin-composer-default": "file:vendor/nodebb-plugin-composer-default", "nodebb-plugin-dbsearch": "6.3.4", "nodebb-plugin-emoji": "6.0.5", "nodebb-plugin-emoji-android": "4.1.1", diff --git a/public/language/ar/topic.json b/public/language/ar/topic.json index 1f5d03920f..9561fadd2b 100644 --- a/public/language/ar/topic.json +++ b/public/language/ar/topic.json @@ -199,6 +199,7 @@ "most-votes": "Most Votes", "most-posts": "Most Posts", "most-views": "Most Views", + "heat": "ساخن", "stale.title": "Create new topic instead?", "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", "stale.create": "موضوع جديد", diff --git a/public/language/az/topic.json b/public/language/az/topic.json index 5a856f5992..70540b1519 100644 --- a/public/language/az/topic.json +++ b/public/language/az/topic.json @@ -199,6 +199,7 @@ "most-votes": "Ən çox səs", "most-posts": "Ən çox yazı", "most-views": "Ən çox baxış", + "heat": "Hot", "stale.title": "Əvəzinə yeni mövzu yaradılsın?", "stale.warning": "Cavab verdiyiniz mövzu olduqca köhnədir. Əvəzində yeni mövzu yaratmaq və cavabınızda bu mövzuya istinad etmək istərdinizmi?", "stale.create": "Yeni mövzu yarat", diff --git a/public/language/bg/topic.json b/public/language/bg/topic.json index baa01a79d9..aa20aad625 100644 --- a/public/language/bg/topic.json +++ b/public/language/bg/topic.json @@ -199,6 +199,7 @@ "most-votes": "Първо тези с най-много гласове", "most-posts": "Първо тези с най-много публикации", "most-views": "Първо тези с най-много преглеждания", + "heat": "Hot", "stale.title": "Създаване на нова тема вместо това?", "stale.warning": "Темата, в която отговаряте, е доста стара. Искате ли вместо това да създадете нова и да направите препратка към тази в отговора си?", "stale.create": "Създаване на нова тема", diff --git a/public/language/bn/topic.json b/public/language/bn/topic.json index 4ee9c37159..068509c4ca 100644 --- a/public/language/bn/topic.json +++ b/public/language/bn/topic.json @@ -199,6 +199,7 @@ "most-votes": "Most Votes", "most-posts": "Most Posts", "most-views": "Most Views", + "heat": "Hot", "stale.title": "Create new topic instead?", "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", "stale.create": "Create a new topic", diff --git a/public/language/cs/topic.json b/public/language/cs/topic.json index a84be0982a..e86685c86c 100644 --- a/public/language/cs/topic.json +++ b/public/language/cs/topic.json @@ -199,6 +199,7 @@ "most-votes": "S nejvíce hlasy", "most-posts": "S nejvíce příspěvky", "most-views": "Nejvíce zobrazení", + "heat": "Hot", "stale.title": "Raději vytvořit nové téma?", "stale.warning": "Reagujete na starší téma. Nechcete raději vytvořit nové téma a na původní v něm odkázat?", "stale.create": "Vytvořit nové téma", diff --git a/public/language/da/topic.json b/public/language/da/topic.json index 7bb88e5473..04ea6f46ef 100644 --- a/public/language/da/topic.json +++ b/public/language/da/topic.json @@ -199,6 +199,7 @@ "most-votes": "Most Votes", "most-posts": "Most Posts", "most-views": "Most Views", + "heat": "Hot", "stale.title": "Opret nyt emne istedet?", "stale.warning": "Emnet du svarer på er ret gammelt. Vil du oprette et nyt emne istedet og referere dette indlæg i dit svar?", "stale.create": "Opret nyt emne", diff --git a/public/language/de/topic.json b/public/language/de/topic.json index 7ddf907569..25cb3a0341 100644 --- a/public/language/de/topic.json +++ b/public/language/de/topic.json @@ -199,6 +199,7 @@ "most-votes": "Meiste Stimmen", "most-posts": "Meiste Beiträge", "most-views": "Die meisten Ansichten", + "heat": "Heiß", "stale.title": "Stattdessen ein neues Thema erstellen?", "stale.warning": "Das Thema auf das du antworten willst ist ziemlich alt. Möchtest du stattdessen ein neues Thema erstellen und auf dieses in deiner Antwort hinweisen?", "stale.create": "Ein neues Thema erstellen", diff --git a/public/language/el/topic.json b/public/language/el/topic.json index 783bf09fb3..4623cc1bee 100644 --- a/public/language/el/topic.json +++ b/public/language/el/topic.json @@ -199,6 +199,7 @@ "most-votes": "Most Votes", "most-posts": "Most Posts", "most-views": "Most Views", + "heat": "Hot", "stale.title": "Create new topic instead?", "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", "stale.create": "Create a new topic", diff --git a/public/language/en-GB/topic.json b/public/language/en-GB/topic.json index f736000fb9..53b81781d3 100644 --- a/public/language/en-GB/topic.json +++ b/public/language/en-GB/topic.json @@ -224,6 +224,7 @@ "most-votes": "Most Votes", "most-posts": "Most Posts", "most-views": "Most Views", + "heat": "Hot", "stale.title": "Create new topic instead?", "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", diff --git a/public/language/en-US/admin/manage/privileges.json b/public/language/en-US/admin/manage/privileges.json index 240cff6aa5..4f84b13e4c 100644 --- a/public/language/en-US/admin/manage/privileges.json +++ b/public/language/en-US/admin/manage/privileges.json @@ -30,6 +30,7 @@ "create-topics": "Create Topics", "reply-to-topics": "Reply to Topics", "schedule-topics": "Schedule Topics", + "verify-posts": "Verify Posts", "tag-topics": "Tag Topics", "edit-posts": "Edit Posts", "view-edit-history": "View Edit History", diff --git a/public/language/en-US/error.json b/public/language/en-US/error.json index ea28a9a51c..ac70bf02bf 100644 --- a/public/language/en-US/error.json +++ b/public/language/en-US/error.json @@ -70,6 +70,8 @@ "category-disabled": "Category disabled", "post-deleted": "Post deleted", "topic-locked": "Topic locked", + "forward-post-same-topic": "Cannot forward a post to its own topic", + "forward-post-choose-topic": "Choose a topic to forward to", "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", diff --git a/public/language/en-US/topic.json b/public/language/en-US/topic.json index 42c3c17e66..a309bc6988 100644 --- a/public/language/en-US/topic.json +++ b/public/language/en-US/topic.json @@ -199,6 +199,7 @@ "most-votes": "Most Votes", "most-posts": "Most Posts", "most-views": "Most Views", + "heat": "Hot", "stale.title": "Create new topic instead?", "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", "stale.create": "Create a new topic", diff --git a/public/language/en-x-pirate/topic.json b/public/language/en-x-pirate/topic.json index 42c3c17e66..a309bc6988 100644 --- a/public/language/en-x-pirate/topic.json +++ b/public/language/en-x-pirate/topic.json @@ -199,6 +199,7 @@ "most-votes": "Most Votes", "most-posts": "Most Posts", "most-views": "Most Views", + "heat": "Hot", "stale.title": "Create new topic instead?", "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", "stale.create": "Create a new topic", diff --git a/public/language/es/topic.json b/public/language/es/topic.json index 10fac46e19..739eaf0af3 100644 --- a/public/language/es/topic.json +++ b/public/language/es/topic.json @@ -199,6 +199,7 @@ "most-votes": "Mayor número de Votos", "most-posts": "Mayor número de Posts", "most-views": "Most Views", + "heat": "Caliente", "stale.title": "¿Crear un nuevo hilo en su lugar?", "stale.warning": "El hilo al que estás respondiendo es muy antiguo. ¿Quieres crear un nuevo hilo en su lugar y añadir una referencia a este en tu mensaje?", "stale.create": "Crear un nuevo hilo", diff --git a/public/language/et/topic.json b/public/language/et/topic.json index 8dce5b3405..870a4fb612 100644 --- a/public/language/et/topic.json +++ b/public/language/et/topic.json @@ -199,6 +199,7 @@ "most-votes": "Most Votes", "most-posts": "Most Posts", "most-views": "Most Views", + "heat": "Hot", "stale.title": "Loo uus teema selle asemel?", "stale.warning": "Teema, millele vastad on küllaltki vana. Kas sooviksid hoopiski uue teema luua ning viidata sellele sinu vastuses?", "stale.create": "Loo uus teema/alapealkiri", diff --git a/public/language/fa-IR/topic.json b/public/language/fa-IR/topic.json index c1a12ef12a..7ae3d7ea75 100644 --- a/public/language/fa-IR/topic.json +++ b/public/language/fa-IR/topic.json @@ -199,6 +199,7 @@ "most-votes": "بیشترین رای ها", "most-posts": "بیشترین پست", "most-views": "بیشترین بازدید‌ها", + "heat": "Hot", "stale.title": "آیا مایلید به جای آن یک موضوع جدید ایجاد کنید؟", "stale.warning": "موضوعی که شما در حال پاسخگویی به آن هستید قدیمی می باشد. آیا میلید به جای آن یک موضوع جدید ایجاد کنید و در آن به این موضوع ارجاع دهید؟", "stale.create": "ایجاد یک موضوع جدید", diff --git a/public/language/fi/topic.json b/public/language/fi/topic.json index ccfcc5d418..ce3c61b7d1 100644 --- a/public/language/fi/topic.json +++ b/public/language/fi/topic.json @@ -199,6 +199,7 @@ "most-votes": "Äänestetyin ensin", "most-posts": "Eniten viestejä", "most-views": "Eniten näyttöjä", + "heat": "Hot", "stale.title": "Create new topic instead?", "stale.warning": "Aihe johon olet vastaamassa on melko vanha. Haluaisitko luoda mieluummin uuden aiheen ja viitata siitä tähän viestissäsi?", "stale.create": "Luo uusi aihe", diff --git a/public/language/fr/topic.json b/public/language/fr/topic.json index 6a28ab9fec..76b63525c5 100644 --- a/public/language/fr/topic.json +++ b/public/language/fr/topic.json @@ -199,6 +199,7 @@ "most-votes": "Les plus votés", "most-posts": "Meilleurs messages", "most-views": "Les plus vus", + "heat": "Brûlant", "stale.title": "Créer un nouveau sujet à la place ?", "stale.warning": "Le sujet auquel vous répondez est assez ancien. Ne voudriez-vous pas créer un nouveau sujet à la place et placer une référence vers celui-ci dans votre réponse ?", "stale.create": "Créer un nouveau sujet", diff --git a/public/language/gl/topic.json b/public/language/gl/topic.json index d29158105c..d0580279e6 100644 --- a/public/language/gl/topic.json +++ b/public/language/gl/topic.json @@ -199,6 +199,7 @@ "most-votes": "Most Votes", "most-posts": "Most Posts", "most-views": "Most Views", + "heat": "Hot", "stale.title": "Crear un novo tema no seu lugar?", "stale.warning": "O tema no que queres publicar é bastante vello. Queres crear un novo tema no seu lugar e incluir unha referencia a este na túa mensaxe?", "stale.create": "Crear un novo tema", diff --git a/public/language/he/topic.json b/public/language/he/topic.json index f63e796f95..0d2bf18548 100644 --- a/public/language/he/topic.json +++ b/public/language/he/topic.json @@ -199,6 +199,7 @@ "most-votes": "הכי הרבה הצבעות", "most-posts": "הכי הרבה פוסטים", "most-views": "הכי הרבה צפיות", + "heat": "Hot", "stale.title": "האם ליצור נושא חדש במקום זאת?", "stale.warning": "הנושא בו אתם מגיבים הוא די ישן. האם ברצונכם לפתוח נושא חדש, ולהזכיר נושא זה בתגובתכם?", "stale.create": "יצירת נושא חדש", diff --git a/public/language/hr/topic.json b/public/language/hr/topic.json index b57a83c3bd..cc6ad463f8 100644 --- a/public/language/hr/topic.json +++ b/public/language/hr/topic.json @@ -199,6 +199,7 @@ "most-votes": "Most Votes", "most-posts": "Most Posts", "most-views": "Most Views", + "heat": "Hot", "stale.title": "Otvori novu temu?", "stale.warning": "Tema na koju odgovarate je stara. Želite li otvoriti novu temu i postaviti referencu u vašem odgovoru?", "stale.create": "Otvori novu temu", diff --git a/public/language/hu/topic.json b/public/language/hu/topic.json index dfad41e707..ffc16066fb 100644 --- a/public/language/hu/topic.json +++ b/public/language/hu/topic.json @@ -199,6 +199,7 @@ "most-votes": "Legtöbb szavazat", "most-posts": "Legtöbb bejegyzés", "most-views": "Legtöbb Megtekintés", + "heat": "Hot", "stale.title": "Inkább új témakör létrehozása?", "stale.warning": "A témakör, melyre válaszolsz, elég régi. Szeretnél helyette inkább új témakört létrehozni, és erre hivatkozni a válaszodban?", "stale.create": "Új témakör létrehozása", diff --git a/public/language/hy/topic.json b/public/language/hy/topic.json index 1fe2851d2f..779ee889a3 100644 --- a/public/language/hy/topic.json +++ b/public/language/hy/topic.json @@ -199,6 +199,7 @@ "most-votes": "Առավելագույն ձայներ", "most-posts": "Ամենաշատ գրառումները", "most-views": "Ամենաշատ դիտումները", + "heat": "Hot", "stale.title": "Փոխարենը ստեղծե՞լ նոր թեմա։", "stale.warning": "Թեման, որում գրառում եք կատարում բավականին հին է։ Կուզե՞ք այստեղ գրելու փոխարեն ստեղծել նոր թեմա՝ Ձեր պատասխանում հղելով այս մեկին։", "stale.create": "Ստեղծել նոր թեմա", diff --git a/public/language/id/topic.json b/public/language/id/topic.json index 599fd67b52..bcca83a614 100644 --- a/public/language/id/topic.json +++ b/public/language/id/topic.json @@ -199,6 +199,7 @@ "most-votes": "Most Votes", "most-posts": "Most Posts", "most-views": "Most Views", + "heat": "Hot", "stale.title": "Create new topic instead?", "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", "stale.create": "Create a new topic", diff --git a/public/language/it/topic.json b/public/language/it/topic.json index 96f61aa439..45267f5277 100644 --- a/public/language/it/topic.json +++ b/public/language/it/topic.json @@ -199,6 +199,7 @@ "most-votes": "Più Voti", "most-posts": "Più Post", "most-views": "Più visualizzazioni", + "heat": "Caldo", "stale.title": "Preferisci creare una nuova discussione?", "stale.warning": "La discussione alla quale stai rispondendo è piuttosto vecchia. Vorresti invece creare una nuova discussione e fare riferimento a questa nella tua risposta?", "stale.create": "Crea una nuova discussione", diff --git a/public/language/ja/topic.json b/public/language/ja/topic.json index 895440aeea..9305973e73 100644 --- a/public/language/ja/topic.json +++ b/public/language/ja/topic.json @@ -199,6 +199,7 @@ "most-votes": "最高評価", "most-posts": "最大投稿", "most-views": "Most Views", + "heat": "Hot", "stale.title": "新しいスレッドを作りますか?", "stale.warning": "あなたが返信しようとしてるスレッドが古いスレッドです。新しいスレッドを作って、そしてこのスレッドが参考として入れた方を勧めます。そうしますか?", "stale.create": "新しいスレッドを作ります。", diff --git a/public/language/ko/topic.json b/public/language/ko/topic.json index f943483ad1..f66226758d 100644 --- a/public/language/ko/topic.json +++ b/public/language/ko/topic.json @@ -199,6 +199,7 @@ "most-votes": "가장 많은 투표", "most-posts": "가장 많은 게시물", "most-views": "가장 많은 조회수", + "heat": "Hot", "stale.title": "새로운 토픽을 생성하시겠습니까?", "stale.warning": "답글을 달고 있는 토픽이 꽤 오래되었습니다. 대신 새로운 토픽을 생성하고 답글에서 이를 참조하시겠습니까?", "stale.create": "새로운 토픽 생성", diff --git a/public/language/lt/topic.json b/public/language/lt/topic.json index b551ce6eb2..6b9e7b7046 100644 --- a/public/language/lt/topic.json +++ b/public/language/lt/topic.json @@ -199,6 +199,7 @@ "most-votes": "Daugiausiai Balsų", "most-posts": "Daugiausiai Įrašų", "most-views": "Most Views", + "heat": "Hot", "stale.title": "Create new topic instead?", "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", "stale.create": "Sukurti naują temą", diff --git a/public/language/lv/topic.json b/public/language/lv/topic.json index e20f510da6..51eb095654 100644 --- a/public/language/lv/topic.json +++ b/public/language/lv/topic.json @@ -199,6 +199,7 @@ "most-votes": "Pēc visvairāk balsojumu", "most-posts": "Pēc visvairāk rakstu", "most-views": "Most Views", + "heat": "Hot", "stale.title": "Tā vietā izveidot jaunu tematu?", "stale.warning": "Šis temats, uz kuru atbildi, ir diezgan sens. Vai vēlies izveidot jaunu tematu un atsaukties uz šo tematu?", "stale.create": "Izveidot jaunu tematu", diff --git a/public/language/ms/topic.json b/public/language/ms/topic.json index 11e3dd2e2a..20659b336b 100644 --- a/public/language/ms/topic.json +++ b/public/language/ms/topic.json @@ -199,6 +199,7 @@ "most-votes": "Most Votes", "most-posts": "Most Posts", "most-views": "Most Views", + "heat": "Hot", "stale.title": "Bukan topik baru?", "stale.warning": "Topik yang anda nak balas agak lapuk. Adakah anda ingin buka topik baru dan rujukkan topik ini dalam balasan anda?", "stale.create": "Buka topik baru", diff --git a/public/language/nb/topic.json b/public/language/nb/topic.json index 8e7ccf54df..c2b46ef91a 100644 --- a/public/language/nb/topic.json +++ b/public/language/nb/topic.json @@ -199,6 +199,7 @@ "most-votes": "Flest anbefalinger", "most-posts": "Flest innlegg", "most-views": "Flest visninger", + "heat": "Hot", "stale.title": "Opprett nytt innlegg i stedet?", "stale.warning": "Innlegget du svarer på er ganske gammelt. Vil du heller lage et nytt innlegg og referere til dette?", "stale.create": "Lag et nytt innlegg", diff --git a/public/language/nl/topic.json b/public/language/nl/topic.json index a4ead8ccd1..cdd1d7bbff 100644 --- a/public/language/nl/topic.json +++ b/public/language/nl/topic.json @@ -199,6 +199,7 @@ "most-votes": "Meeste stemmen", "most-posts": "Meeste berichten", "most-views": "Most Views", + "heat": "Heet", "stale.title": "Een nieuw onderwerp maken in de plaats?", "stale.warning": "Het onderwerp waar je op antwoord is vrij oud. Zou je graag een nieuw onderwerp maken met een referentie naar dit onderwerp in je antwoord?", "stale.create": "Maak een nieuw onderwerp", diff --git a/public/language/nn-NO/topic.json b/public/language/nn-NO/topic.json index 1e757aa136..f224428cd0 100644 --- a/public/language/nn-NO/topic.json +++ b/public/language/nn-NO/topic.json @@ -199,6 +199,7 @@ "most-votes": "Flest anbefalingar", "most-posts": "Fleire innlegg", "most-views": "Fleire visningar", + "heat": "Hot", "stale.title": "Opprett nytt innlegg i staden?", "stale.warning": "Innlegget du svarer på er gammalt. Ønskjer du å opprette eit nytt innlegg i staden, og referere til dette i svaret ditt?", "stale.create": "Opprett nytt innlegg", diff --git a/public/language/pl/topic.json b/public/language/pl/topic.json index 7817083a3a..75c42951d6 100644 --- a/public/language/pl/topic.json +++ b/public/language/pl/topic.json @@ -199,6 +199,7 @@ "most-votes": "Najwięcej głosów", "most-posts": "Najwięcej postów", "most-views": "Najwięcej wyświetleń", + "heat": "Hot", "stale.title": "Stworzyć nowy temat?", "stale.warning": "Temat, na który chcesz udzielić odpowiedzi, jest dość stary. Czy nie wolisz utworzyć nowego tematu i jedynie odnieść się do tego?", "stale.create": "Stwórz nowy temat", diff --git a/public/language/pt-BR/topic.json b/public/language/pt-BR/topic.json index 785decb74c..81298daa46 100644 --- a/public/language/pt-BR/topic.json +++ b/public/language/pt-BR/topic.json @@ -199,6 +199,7 @@ "most-votes": "Mais Votados", "most-posts": "Mais Postagens", "most-views": "Mais Views", + "heat": "Quente", "stale.title": "Criar um novo tópico ao invés disso?", "stale.warning": "O tópico que você está respondendo é bem antigo. Você gostaria de criar um novo tópico ao invés disso, e referenciá-lo em sua resposta?", "stale.create": "Criar um novo tópico", diff --git a/public/language/pt-PT/topic.json b/public/language/pt-PT/topic.json index b32e1a9889..0c571a5211 100644 --- a/public/language/pt-PT/topic.json +++ b/public/language/pt-PT/topic.json @@ -199,6 +199,7 @@ "most-votes": "Mais votos", "most-posts": "Mais publicações", "most-views": "Most Views", + "heat": "Hot", "stale.title": "Em vez disso, criar novo tópico?", "stale.warning": "O tópico ao qual estás a responder é bastante antigo. Gostarias antes de criar um novo tópico e referir este na tua resposta?", "stale.create": "Criar um novo tópico", diff --git a/public/language/ro/topic.json b/public/language/ro/topic.json index b52960064a..39a1b279f4 100644 --- a/public/language/ro/topic.json +++ b/public/language/ro/topic.json @@ -199,6 +199,7 @@ "most-votes": "Most Votes", "most-posts": "Most Posts", "most-views": "Most Views", + "heat": "Hot", "stale.title": "Create new topic instead?", "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", "stale.create": "Create a new topic", diff --git a/public/language/ru/topic.json b/public/language/ru/topic.json index 40153c951d..52aa666ecb 100644 --- a/public/language/ru/topic.json +++ b/public/language/ru/topic.json @@ -199,6 +199,7 @@ "most-votes": "По количеству голосов", "most-posts": "По количеству сообщений", "most-views": "Наиболее Просматриваемые", + "heat": "Горячо", "stale.title": "Создать новую тему вместо этой?", "stale.warning": "Тема, в которую вы собираетесь написать, очень старая. Может, стоит создать новую, а про эту просто напомнить к случаю?", "stale.create": "Создать новую тему", diff --git a/public/language/rw/topic.json b/public/language/rw/topic.json index cc9f436d2c..1955c0834f 100644 --- a/public/language/rw/topic.json +++ b/public/language/rw/topic.json @@ -199,6 +199,7 @@ "most-votes": "Most Votes", "most-posts": "Most Posts", "most-views": "Most Views", + "heat": "Hot", "stale.title": "Urashaka gutangiza ahubwo ikiganiro gishya?", "stale.warning": "Ikiganiro ushaka kuvugaho cyarashaje. Wahitamo gutangiza ikiganiro gishya ariko wenda ukagaragaza kino mu gisubizo uza gushyiraho?", "stale.create": "Tangiza ikiganiro gishya", diff --git a/public/language/sc/topic.json b/public/language/sc/topic.json index 37cc4037ad..c9a9fcbfec 100644 --- a/public/language/sc/topic.json +++ b/public/language/sc/topic.json @@ -199,6 +199,7 @@ "most-votes": "Most Votes", "most-posts": "Most Posts", "most-views": "Most Views", + "heat": "Hot", "stale.title": "Create new topic instead?", "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", "stale.create": "Create a new topic", diff --git a/public/language/sk/topic.json b/public/language/sk/topic.json index 7323338171..cb9af7e778 100644 --- a/public/language/sk/topic.json +++ b/public/language/sk/topic.json @@ -199,6 +199,7 @@ "most-votes": "S najviac hlasmi", "most-posts": "S najviac príspevkami", "most-views": "Most Views", + "heat": "Hot", "stale.title": "Vytvoriť novú tému namiesto?", "stale.warning": "Téma na ktorú odpovedáte je pomerne stará. Chceli by ste vytvoriť novú tému namiesto tejto, a odkazovať na ňu vo Vašej odpovedi?", "stale.create": "Vytvoriť novú tému", diff --git a/public/language/sl/topic.json b/public/language/sl/topic.json index 02ec954c1d..4dfb49abe7 100644 --- a/public/language/sl/topic.json +++ b/public/language/sl/topic.json @@ -199,6 +199,7 @@ "most-votes": "Največ glasov", "most-posts": "Največ objav", "most-views": "Največ ogledov", + "heat": "Hot", "stale.title": "Raje ustvari novo temo?", "stale.warning": "Tema na katero odgovarjaš je precej stara. A ne bi raje ustvaril novo temo namesto te, z sklicem na to v tvojem odgovoru?", "stale.create": "Ustvari novo temo", diff --git a/public/language/sq-AL/topic.json b/public/language/sq-AL/topic.json index a4aeccae0a..db4db84b00 100644 --- a/public/language/sq-AL/topic.json +++ b/public/language/sq-AL/topic.json @@ -199,6 +199,7 @@ "most-votes": "Më të votuarat", "most-posts": "Të gjitha postimet", "most-views": "Më të shikuarat", + "heat": "Hot", "stale.title": "Krijo një temë të re më mirë?", "stale.warning": "Tema që po i përgjigjesh është shumë e vjetër. Dëshironi të krijoni një temë të re në vend të saj dhe t'i referoheni në përgjigjen tuaj?", "stale.create": "Krijo një temë të re", diff --git a/public/language/sr/topic.json b/public/language/sr/topic.json index 18fd58fd83..6b280748e4 100644 --- a/public/language/sr/topic.json +++ b/public/language/sr/topic.json @@ -199,6 +199,7 @@ "most-votes": "Највише гласова", "most-posts": "Највише порука", "most-views": "Највише прегледа", + "heat": "Hot", "stale.title": "Креирати нову тему уместо тога?", "stale.warning": "Тема у којој желите да одговорите је сувише стара. Да ли желите да уместо тога креирате нову тему и упутите на ову у вашем одговору?", "stale.create": "Креирај нову тему", diff --git a/public/language/sv/topic.json b/public/language/sv/topic.json index 1097c11638..b402b4c6c2 100644 --- a/public/language/sv/topic.json +++ b/public/language/sv/topic.json @@ -199,6 +199,7 @@ "most-votes": "Flest röster", "most-posts": "Flest inlägg", "most-views": "Flest visningar", + "heat": "Hot", "stale.title": "Skapa nytt ämne istället?", "stale.warning": "Ämnet du svarar på är ganska gammalt. Vill du skapa ett nytt ämne istället och inkludera en referens till det här ämnet i ditt inlägg?", "stale.create": "Skapa nytt ämne", diff --git a/public/language/th/topic.json b/public/language/th/topic.json index 5562e20295..af54705ffa 100644 --- a/public/language/th/topic.json +++ b/public/language/th/topic.json @@ -199,6 +199,7 @@ "most-votes": "โหวดมากสุด", "most-posts": "โพสต์มากสุด", "most-views": "จำนวนวิวมากสุด", + "heat": "Hot", "stale.title": "ตั้งกระทู้ใหม่แทนไหม?", "stale.warning": "กระทู้ที่คุณกำลังตอบเก่าไปหน่อยนะ อยากจะลองตั้งกระทู้ใหม่แทนไหมล่ะ? แล้วก็อ้างอิงกระทู้นี้ไปยังคำตอบของคุณ", "stale.create": "ตั้งกระทู้ใหม่", diff --git a/public/language/tr/topic.json b/public/language/tr/topic.json index 0bddc42542..f917d92f1d 100644 --- a/public/language/tr/topic.json +++ b/public/language/tr/topic.json @@ -199,6 +199,7 @@ "most-votes": "En çok oylanan", "most-posts": "En çok ileti yazılan", "most-views": "Çok Görüntülenen", + "heat": "Hot", "stale.title": "Bunun yerine yeni bir başlık oluşturun?", "stale.warning": "Yanıtlamak istediğiniz başlık oldukça eski. Bu başlığa referans oluşturacak yeni bir başlık oluşturmak ister misiniz?", "stale.create": "Yeni bir başlık oluştur", diff --git a/public/language/uk/topic.json b/public/language/uk/topic.json index 70128938ef..639fccc795 100644 --- a/public/language/uk/topic.json +++ b/public/language/uk/topic.json @@ -199,6 +199,7 @@ "most-votes": "Найбільше Голосів", "most-posts": "Найбільше Постів", "most-views": "Most Views", + "heat": "Hot", "stale.title": "Створити натомість нову тему?", "stale.warning": "Тема на котру ви відповідаєте досить стара. Не бажаєте натомість створити новую тему і зіслатися на цю у вашій відповіді?", "stale.create": "Так, створити нову тему", diff --git a/public/language/ur/topic.json b/public/language/ur/topic.json index 5c6e9f7991..13628e54ea 100644 --- a/public/language/ur/topic.json +++ b/public/language/ur/topic.json @@ -199,6 +199,7 @@ "most-votes": "پہلے سب سے زیادہ ووٹ والے", "most-posts": "پہلے سب سے زیادہ پوسٹس والے", "most-views": "پہلے سب سے زیادہ نظاروں والے", + "heat": "Hot", "stale.title": "اس کے بجائے نیا موضوع بنائیں؟", "stale.warning": "جس موضوع میں آپ جواب دے رہے ہیں وہ کافی پرانا ہے۔ کیا آپ اس کے بجائے ایک نیا موضوع بنانا چاہیں گے اور اپنے جواب میں اس کا حوالہ دیں گے؟", "stale.create": "نیا موضوع بنائیں", diff --git a/public/language/vi/topic.json b/public/language/vi/topic.json index 63c5f43ae1..5aa414a001 100644 --- a/public/language/vi/topic.json +++ b/public/language/vi/topic.json @@ -199,6 +199,7 @@ "most-votes": "Nhiều Bình Chọn Nhất", "most-posts": "Nhiều Bài Đăng Nhất", "most-views": "Xem Nhiều Nhất", + "heat": "Hot", "stale.title": "Tạo chủ đề mới thay thế?", "stale.warning": "Chủ đề bạn đang trả lời đã khá cũ. Thay vào đó, bạn có muốn tạo một chủ đề mới và tham khảo phần này trong câu trả lời của bạn không?", "stale.create": "Tạo một chủ đề mới", diff --git a/public/language/zh-CN/topic.json b/public/language/zh-CN/topic.json index 8439c99bee..8e2da9ec34 100644 --- a/public/language/zh-CN/topic.json +++ b/public/language/zh-CN/topic.json @@ -199,6 +199,7 @@ "most-votes": "最多赞同", "most-posts": "回复最多", "most-views": "最多浏览", + "heat": "热门", "stale.title": "接受建议并创建新主题?", "stale.warning": "您回复的主题已经很古老了。您是否用发布新主题代替,并引用此主题的内容?", "stale.create": "创建新主题", diff --git a/public/language/zh-TW/topic.json b/public/language/zh-TW/topic.json index ab27551f77..57d61c0c7e 100644 --- a/public/language/zh-TW/topic.json +++ b/public/language/zh-TW/topic.json @@ -199,6 +199,7 @@ "most-votes": "最多點贊", "most-posts": "回覆最多", "most-views": "最多觀看", + "heat": "Hot", "stale.title": "接受建議並建立新主題?", "stale.warning": "您回覆的主題已經很古老了,是否發佈新主題並引用此主題的內容?", "stale.create": "建立新主題", diff --git a/public/openapi/components/schemas/PostObject.yaml b/public/openapi/components/schemas/PostObject.yaml index 1904cded51..f6abfc8ea2 100644 --- a/public/openapi/components/schemas/PostObject.yaml +++ b/public/openapi/components/schemas/PostObject.yaml @@ -170,6 +170,9 @@ PostDataObject: description: A topic identifier content: type: string + anonymous: + type: string + default: 'false' timestamp: type: number votes: diff --git a/public/src/client/topic.js b/public/src/client/topic.js index b54185bf80..e30d27f018 100644 --- a/public/src/client/topic.js +++ b/public/src/client/topic.js @@ -76,6 +76,17 @@ define('forum/topic', [ handleTopicSearch(); hooks.fire('action:topic.loaded', ajaxify.data); + + require(['forum/topic/forward-post'], function (forwardPost) { + const stored = forwardPost.getStoredForward(); + if (stored && String(stored.tid) === String(ajaxify.data.tid) && stored.body) { + hooks.fire('action:composer.post.new', { + tid: ajaxify.data.tid, + title: ajaxify.data.titleRaw || stored.title, + body: stored.body, + }); + } + }); }; function handleTopicSearch() { diff --git a/public/src/client/topic/forward-post.js b/public/src/client/topic/forward-post.js new file mode 100644 index 0000000000..96e04dd320 --- /dev/null +++ b/public/src/client/topic/forward-post.js @@ -0,0 +1,114 @@ +'use strict'; + +define('forum/topic/forward-post', [ + 'api', 'alerts', 'search', 'hooks', +], function (api, alerts, search, hooks) { + const ForwardPost = {}; + const FORWARD_STORAGE_KEY = 'nodebb_forward_reply'; + + // Block submitting the reply if user has not chosen a destination topic (composer still has forward search bar) + hooks.on('filter:composer.check', function (payload) { + if (payload.postContainer && payload.postContainer.find('.forward-post-search').length) { + payload.error = '[[error:forward-post-choose-topic]]'; + } + }); + + // Add topic search bar into the composer + $(window).on('action:composer.loaded', function (ev, data) { + if (!app.forwardPostContext || !data || !data.postContainer) { + return; + } + + const postContainer = data.postContainer; + const ctx = app.forwardPostContext; + app.forwardPostContext = null; + + const header = $('
'); + header.append( + $('
').text('Forward this post to another topic:') + ); + + const inputGroup = $( + '
' + + '' + + '' + + '
' + ); + const resultsContainer = $( + '' + ); + + header.append(inputGroup); + header.append(resultsContainer); + + postContainer.find('.composer-container, .title-container, .composer').first().before(header); + + if (!config.searchEnabled || !app.user.privileges['search:content']) { + inputGroup.find('.forward-topic-search-input') + .attr('disabled', true) + .attr('placeholder', 'Search is disabled'); + return; + } + + search.enableQuickSearch({ + searchElements: { + inputEl: header.find('.forward-topic-search-input'), + resultEl: resultsContainer, + }, + searchOptions: { + in: 'titles', + }, + }); + + header.on('click', '.quick-search-results [data-tid]', function (e) { + e.preventDefault(); + e.stopPropagation(); + const tid = $(this).attr('data-tid'); + if (!tid) { + return; + } + // Throw error if user forwards post to the post's topic + if (ajaxify.data.tid && String(tid) === String(ajaxify.data.tid)) { + return alerts.error('[[error:forward-post-same-topic]]'); + } + api.get(`/topics/${tid}`, {}).then(function (topicData) { + if (!topicData || !topicData.slug) { + return alerts.error('[[error:no-topic]]'); + } + const currentBody = (postContainer.find('textarea').val() || '').trim() || (ctx.body || ''); + try { + sessionStorage.setItem(FORWARD_STORAGE_KEY, JSON.stringify({ + tid: String(tid), + title: topicData.title || '', + body: currentBody, + })); + } catch (err) { + return alerts.error(err); + } + + // Close the current composer (if possible) + postContainer.find('[component="composer/close"]').trigger('click'); + + ajaxify.go('topic/' + topicData.slug); + }).catch(alerts.error); + return false; + }); + }); + + ForwardPost.getStoredForward = function () { + try { + const raw = sessionStorage.getItem(FORWARD_STORAGE_KEY); + if (!raw) return null; + const data = JSON.parse(raw); + sessionStorage.removeItem(FORWARD_STORAGE_KEY); + return data; + } catch (e) { + return null; + } + }; + + return ForwardPost; +}); diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js index 640d936f16..060dbe94a3 100644 --- a/public/src/client/topic/postTools.js +++ b/public/src/client/topic/postTools.js @@ -76,7 +76,7 @@ define('forum/topic/postTools', [ PostTools.toggle = function (pid, isDeleted) { const postEl = components.get('post', 'pid', pid); - postEl.find('[component="post/quote"], [component="post/bookmark"], [component="post/reply"], [component="post/flag"], [component="user/chat"]') + postEl.find('[component="post/quote"], [component="post/bookmark"], [component="post/reply"], [component="post/forward"], [component="post/flag"], [component="user/chat"]') .toggleClass('hidden', isDeleted); postEl.find('[component="post/delete"]').toggleClass('hidden', isDeleted).parent().attr('hidden', isDeleted ? '' : null); @@ -116,6 +116,11 @@ define('forum/topic/postTools', [ onReplyClicked($(this), tid); }); + postContainer.on('click', '[component="post/forward"]', function (e) { + e.preventDefault(); + onForwardClicked($(this), tid); + }); + $('.topic').on('click', '[component="topic/reply-as-topic"]', function () { translator.translate(`[[topic:link-back, ${ajaxify.data.titleRaw}, ${config.relative_path}/topic/${ajaxify.data.slug}]]`, function (body) { hooks.fire('action:composer.topic.new', { @@ -324,6 +329,32 @@ define('forum/topic/postTools', [ }); } + async function onForwardClicked(button, tid) { + const pid = getData(button, 'data-pid'); + if (!pid) return; + + try { + const { content } = await api.get(`/posts/${encodeURIComponent(pid)}/raw`); + const postUrl = config.relative_path + '/post/' + encodeURIComponent(pid); + const title = ajaxify.data.titleRaw || ''; + const forwardedHeader = '> ' + (title ? '**Forwarded from** [' + title.replace(/\]/g, '\\]') + '](' + postUrl + ')' : postUrl); + const quotedContent = (content || '').split('\n').map(function (line) { return '> ' + line; }).join('\n'); + const forwardBody = forwardedHeader + (quotedContent ? '\n' + quotedContent : ''); + + app.forwardPostContext = { + body: forwardBody, + }; + + hooks.fire('action:composer.post.new', { + tid: tid, + title: ajaxify.data.titleRaw, + body: forwardBody, + }); + } catch (err) { + alerts.error(err); + } + } + async function onQuoteClicked(button, tid) { const selectedNode = await getSelectedNode(); diff --git a/public/src/client/topic/threadTools.js b/public/src/client/topic/threadTools.js index 7e1a001ada..0ff5927350 100644 --- a/public/src/client/topic/threadTools.js +++ b/public/src/client/topic/threadTools.js @@ -326,7 +326,7 @@ define('forum/topic/threadTools', [ components.get('topic/reply/container').toggleClass('hidden', hideReply); components.get('topic/reply/locked').toggleClass('hidden', ajaxify.data.privileges.isAdminOrMod || !data.isLocked || ajaxify.data.deleted); - threadEl.find('[component="post"]:not(.deleted) [component="post/reply"], [component="post"]:not(.deleted) [component="post/quote"]').toggleClass('hidden', hideReply); + threadEl.find('[component="post"]:not(.deleted) [component="post/reply"], [component="post"]:not(.deleted) [component="post/quote"], [component="post"]:not(.deleted) [component="post/forward"]').toggleClass('hidden', hideReply); threadEl.find('[component="post/edit"], [component="post/delete"]').toggleClass('hidden', isLocked); threadEl.find('[component="post"][data-uid="' + app.user.uid + '"].deleted [component="post/tools"]').toggleClass('hidden', isLocked); diff --git a/public/src/modules/quickreply.js b/public/src/modules/quickreply.js index f9c74e4b34..cc5da75e7d 100644 --- a/public/src/modules/quickreply.js +++ b/public/src/modules/quickreply.js @@ -59,12 +59,15 @@ define('quickreply', [ if (!ready) { return; } - + + var anonymous = (components.get('topic/quickreply/anonymize').prop('checked')) ? 'true' : 'false'; + const replyMsg = element.val(); const replyData = { tid: ajaxify.data.tid, handle: undefined, content: replyMsg, + anonymous: anonymous, }; const replyLen = replyMsg.length; if (replyLen < parseInt(config.minimumPostLength, 10)) { @@ -82,21 +85,24 @@ define('quickreply', [ return alerts.error(err); } if (data && data.queued) { + data.anonymous = anonymous; alerts.alert({ type: 'success', title: '[[global:alert.success]]', message: data.message, timeout: 10000, + anonymous: anonymous, clickfn: function () { ajaxify.go(`/post-queue/${data.id}`); }, }); } - + data.anonymous = anonymous; element.val(''); storage.removeItem(qrDraftId); QuickReply._autocomplete.hide(); hooks.fire('action:quickreply.success', { data }); + ajaxify.refresh(); }); }); diff --git a/retire-report.json b/retire-report.json new file mode 100644 index 0000000000..3d6f4dc681 --- /dev/null +++ b/retire-report.json @@ -0,0 +1 @@ +{"version":"5.4.2","start":"2026-03-11T21:20:43.864Z","data":[{"file":"/workspaces/nodebb-spring-26-team-bing/node_modules/faker/locale/.publish/scripts/docstrap.lib.js","results":[{"version":"2.1.4","component":"jquery","npmname":"jquery","detection":"filecontent","vulnerabilities":[{"info":["http://research.insecurelabs.org/jquery/test/","https://bugs.jquery.com/ticket/11974"],"below":"2.2.0","atOrAbove":"1.8.0","severity":"medium","identifiers":{"summary":"parseHTML() executes scripts in event handlers","issue":"11974"},"cwe":["CWE-79"]},{"info":["https://github.com/jquery/jquery.com/issues/162"],"below":"2.999.999","severity":"low","identifiers":{"summary":"jQuery 1.x and 2.x are End-of-Life and no longer receiving security updates","retid":"73","issue":"162"},"cwe":["CWE-1104"]},{"info":["http://blog.jquery.com/2016/01/08/jquery-2-2-and-1-12-released/","http://research.insecurelabs.org/jquery/test/","https://github.com/advisories/GHSA-rmxg-73gg-4p98","https://github.com/jquery/jquery/issues/2432","https://nvd.nist.gov/vuln/detail/CVE-2015-9251"],"below":"3.0.0-beta1","atOrAbove":"1.12.3","severity":"medium","identifiers":{"summary":"3rd party CORS request may execute","issue":"2432","CVE":["CVE-2015-9251"],"githubID":"GHSA-rmxg-73gg-4p98"},"cwe":["CWE-79"]},{"info":["https://blog.jquery.com/2019/04/10/jquery-3-4-0-released/","https://github.com/jquery/jquery/commit/753d591aea698e57d6db58c9f722cd0808619b1b","https://nvd.nist.gov/vuln/detail/CVE-2019-11358"],"below":"3.4.0","atOrAbove":"1.1.4","severity":"medium","identifiers":{"summary":"jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution","CVE":["CVE-2019-11358"],"PR":"4333","githubID":"GHSA-6c3j-c64m-qhgq"},"cwe":["CWE-1321","CWE-79"]},{"info":["https://blog.jquery.com/2020/04/10/jquery-3-5-0-released/"],"below":"3.5.0","atOrAbove":"1.0.3","severity":"medium","identifiers":{"summary":"passing HTML containing