Merge remote-tracking branch 'origin/develop' into activitypub

This commit is contained in:
Julian Lam
2024-01-12 15:23:41 -05:00
79 changed files with 912 additions and 46 deletions

View File

@@ -1,3 +1,106 @@
#### v3.6.3 (2024-01-12)
##### Chores
* incrementing version number - v3.6.2 (0f577a57)
* update changelog for v3.6.2 (82a936c3)
* incrementing version number - v3.6.1 (f1a69468)
* incrementing version number - v3.6.0 (4cdf85f8)
* incrementing version number - v3.5.3 (ed0e8783)
* incrementing version number - v3.5.2 (52fbb2da)
* incrementing version number - v3.5.1 (4c543488)
* incrementing version number - v3.5.0 (d06fb4f0)
* incrementing version number - v3.4.3 (5c984250)
* incrementing version number - v3.4.2 (3f0dac38)
* incrementing version number - v3.4.1 (01e69574)
* incrementing version number - v3.4.0 (fd9247c5)
* incrementing version number - v3.3.9 (5805e770)
* incrementing version number - v3.3.8 (a5603565)
* incrementing version number - v3.3.7 (b26f1744)
* incrementing version number - v3.3.6 (7fb38792)
* incrementing version number - v3.3.4 (a67f84ea)
* incrementing version number - v3.3.3 (f94d239b)
* incrementing version number - v3.3.2 (ec9dac97)
* incrementing version number - v3.3.1 (151cc68f)
* incrementing version number - v3.3.0 (fc1ad70f)
* incrementing version number - v3.2.3 (b06d3e63)
* incrementing version number - v3.2.2 (758ecfcd)
* incrementing version number - v3.2.1 (20145074)
* incrementing version number - v3.2.0 (9ecac38e)
* incrementing version number - v3.1.7 (0b4e81ab)
* incrementing version number - v3.1.6 (b3a3b130)
* incrementing version number - v3.1.5 (ec19343a)
* incrementing version number - v3.1.4 (2452783c)
* incrementing version number - v3.1.3 (3b4e9d3f)
* incrementing version number - v3.1.2 (40fa3489)
* incrementing version number - v3.1.1 (40250733)
* incrementing version number - v3.1.0 (0cb386bd)
* incrementing version number - v3.0.1 (26f6ea49)
* incrementing version number - v3.0.0 (224e08cd)
##### Bug Fixes
* #12275, pin sharp to 0.32.6 (f3927ce7)
* topic event translations closes #12273 (5f91cc83)
#### v3.6.2 (2024-01-10)
##### Chores
* up composer (ef8f8db7)
* up harmony (2bed405c)
* up harmony (b6dbe1a6)
* up lavender (16f0affa)
* up themes (980bfee8)
* incrementing version number - v3.6.1 (f1a69468)
* update changelog for v3.6.1 (1e4abdbf)
* incrementing version number - v3.6.0 (4cdf85f8)
* incrementing version number - v3.5.3 (ed0e8783)
* incrementing version number - v3.5.2 (52fbb2da)
* incrementing version number - v3.5.1 (4c543488)
* incrementing version number - v3.5.0 (d06fb4f0)
* incrementing version number - v3.4.3 (5c984250)
* incrementing version number - v3.4.2 (3f0dac38)
* incrementing version number - v3.4.1 (01e69574)
* incrementing version number - v3.4.0 (fd9247c5)
* incrementing version number - v3.3.9 (5805e770)
* incrementing version number - v3.3.8 (a5603565)
* incrementing version number - v3.3.7 (b26f1744)
* incrementing version number - v3.3.6 (7fb38792)
* incrementing version number - v3.3.4 (a67f84ea)
* incrementing version number - v3.3.3 (f94d239b)
* incrementing version number - v3.3.2 (ec9dac97)
* incrementing version number - v3.3.1 (151cc68f)
* incrementing version number - v3.3.0 (fc1ad70f)
* incrementing version number - v3.2.3 (b06d3e63)
* incrementing version number - v3.2.2 (758ecfcd)
* incrementing version number - v3.2.1 (20145074)
* incrementing version number - v3.2.0 (9ecac38e)
* incrementing version number - v3.1.7 (0b4e81ab)
* incrementing version number - v3.1.6 (b3a3b130)
* incrementing version number - v3.1.5 (ec19343a)
* incrementing version number - v3.1.4 (2452783c)
* incrementing version number - v3.1.3 (3b4e9d3f)
* incrementing version number - v3.1.2 (40fa3489)
* incrementing version number - v3.1.1 (40250733)
* incrementing version number - v3.1.0 (0cb386bd)
* incrementing version number - v3.0.1 (26f6ea49)
* incrementing version number - v3.0.0 (224e08cd)
##### New Features
* #12247 (0af19afd)
##### Bug Fixes
* recent loading (d3d6d77d)
* closes #12246, direction is passed in as string as query param with new api call (8867f243)
* stricter selector for sort (a74b5141)
##### Performance Improvements
* dont load all followed tids on unread/recent?filter=watched (563e03b6)
#### v3.6.1 (2023-12-22)
##### Chores

View File

@@ -2,7 +2,7 @@
"name": "nodebb",
"license": "GPL-3.0",
"description": "NodeBB Forum",
"version": "3.6.1",
"version": "3.6.3",
"homepage": "https://www.nodebb.org",
"repository": {
"type": "git",
@@ -94,7 +94,7 @@
"multiparty": "4.2.3",
"nconf": "0.12.1",
"nodebb-plugin-2factor": "7.4.0",
"nodebb-plugin-composer-default": "10.2.30",
"nodebb-plugin-composer-default": "10.2.31",
"nodebb-plugin-dbsearch": "6.2.3",
"nodebb-plugin-emoji": "5.1.13",
"nodebb-plugin-emoji-android": "4.0.0",
@@ -103,10 +103,10 @@
"nodebb-plugin-ntfy": "1.7.3",
"nodebb-plugin-spam-be-gone": "2.2.0",
"nodebb-rewards-essentials": "1.0.0",
"nodebb-theme-harmony": "1.1.108",
"nodebb-theme-harmony": "1.2.3",
"nodebb-theme-lavender": "7.1.7",
"nodebb-theme-peace": "2.1.26",
"nodebb-theme-persona": "13.2.50",
"nodebb-theme-peace": "2.2.0",
"nodebb-theme-persona": "13.3.1",
"nodebb-widget-essentials": "7.0.14",
"nodemailer": "6.9.8",
"nprogress": "0.2.0",
@@ -127,10 +127,10 @@
"sass": "1.69.7",
"semver": "7.5.4",
"serve-favicon": "2.5.0",
"sharp": "0.33.1",
"sharp": "0.32.6",
"sitemap": "7.1.1",
"socket.io": "4.7.3",
"socket.io-client": "4.7.3",
"socket.io": "4.7.4",
"socket.io-client": "4.7.4",
"@socket.io/redis-adapter": "8.2.1",
"sortablejs": "1.15.1",
"spdx-license-list": "6.8.0",
@@ -163,7 +163,7 @@
"grunt": "1.6.1",
"grunt-contrib-watch": "1.1.0",
"husky": "8.0.3",
"jsdom": "23.0.1",
"jsdom": "23.2.0",
"lint-staged": "15.2.0",
"mocha": "10.2.0",
"mocha-lcov-reporter": "1.3.0",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "В заглавията",
"in-titles-posts": "В заглавията и публикациите",
"in-posts": "В публикациите",
"in-bookmarks": "In bookmarks",
"in-categories": "В категориите",
"in-users": "В потребителите",
"in-tags": "В етикетите",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "In Titeln",
"in-titles-posts": "In Titeln und Beiträgen",
"in-posts": "In Beiträgen",
"in-bookmarks": "In bookmarks",
"in-categories": "In Kategorien",
"in-users": "In Benutzern",
"in-tags": "In Tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "En títulos",
"in-titles-posts": "En títulos y posts",
"in-posts": "En posts",
"in-bookmarks": "In bookmarks",
"in-categories": "En categorías",
"in-users": "En usuarios",
"in-tags": "En etiquetas",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "در دسته‌بندی‌ها",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "Dans les titres",
"in-titles-posts": "Dans les titres et messages",
"in-posts": "Dans les messages",
"in-bookmarks": "In bookmarks",
"in-categories": "Dans les catégories",
"in-users": "Dans les utilisateurs",
"in-tags": "Dans les mots-clés",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -32,7 +32,7 @@
"folder-exists": "התיקיה קיימת",
"invalid-pagination-value": "ערך דף לא חוקי, חייב להיות לפחות %1 ולא מעל %2",
"username-taken": "שם משתמש תפוס",
"email-taken": "Email address is already taken.",
"email-taken": "כתובת האימייל כבר תפוסה.",
"email-nochange": "כתובת דוא\"ל שהוזן זהה לדוא\"ל שנמצא כבר",
"email-invited": "נשלחה כבר הזמנה לדוא\"ל זה",
"email-not-confirmed": "פרסום בקטגוריות או בנושאים מסוימים מופעל רק לאחר אישור הדוא\"ל שלכם, אנא לחצו כאן כדי לשלוח אימות לדוא\"ל שלכם.",

View File

@@ -7,6 +7,7 @@
"in-titles": "בכותרות הנושאים",
"in-titles-posts": "בכותרות ובפוסטים",
"in-posts": "בפוסטים",
"in-bookmarks": "In bookmarks",
"in-categories": "בקטגוריות",
"in-users": "במשתמשים",
"in-tags": "בתגיות",

View File

@@ -4,6 +4,6 @@
"upload-success": "הקובץ הועלה בהצלחה!",
"maximum-file-size": "מקסימום %1 קילובייט",
"no-uploads-found": "לא נמצאו העלאות!",
"public-uploads-info": "העלאות הינם ציבוריות. כל הגולשים יוכלו לראותם.",
"public-uploads-info": "העלאות הינם ציבוריות. כל מי שיש ברשותו לינק לקובץ יוכל לראות אותו.",
"private-uploads-info": "העלאות הינם פרטיות. רק משתמשים מחוברים יוכלו לראותם."
}

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "Címekben",
"in-titles-posts": "Címekben és bejegyzésekben",
"in-posts": "Bejegyzésekben",
"in-bookmarks": "In bookmarks",
"in-categories": "Kategóriákban",
"in-users": "Felhasználóknál",
"in-tags": "Címkékben",

View File

@@ -7,6 +7,7 @@
"in-titles": "Վերնագրերում",
"in-titles-posts": "Վերնագրերում և գրառումներում",
"in-posts": "Գրառումներում",
"in-bookmarks": "In bookmarks",
"in-categories": "Կատեգորիաներում",
"in-users": "Օգտատերերում",
"in-tags": "Պիտակներում",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "Nei titoli",
"in-titles-posts": "Nei titoli e nei post",
"in-posts": "Nei post",
"in-bookmarks": "In bookmarks",
"in-categories": "Nelle categorie",
"in-users": "Negli utenti",
"in-tags": "Nei tag",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categorieën",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "W tytułach",
"in-titles-posts": "W tytułach i postach",
"in-posts": "W postach",
"in-bookmarks": "In bookmarks",
"in-categories": "W kategoriach",
"in-users": "W użytkownikach",
"in-tags": "W tagach",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "В заголовках",
"in-titles-posts": "В заголовках и постах",
"in-posts": "В постах",
"in-bookmarks": "In bookmarks",
"in-categories": "В категориях",
"in-users": "В пользователях",
"in-tags": "В тегах",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "У насловима",
"in-titles-posts": "У насловима и објавама",
"in-posts": "У објавама",
"in-bookmarks": "In bookmarks",
"in-categories": "У категоријама",
"in-users": "У корисницима",
"in-tags": "У ознакама",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "Başlıklarda",
"in-titles-posts": "Başlıklarda ve iletilerde",
"in-posts": "İletilerde",
"in-bookmarks": "In bookmarks",
"in-categories": "Kategorilerde",
"in-users": "Kullanıcılarda",
"in-tags": "Etiketlerde",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -7,6 +7,7 @@
"in-titles": "Trong tiêu đề",
"in-titles-posts": "Trong tiêu đề và bài đăng",
"in-posts": "Trong bài đăng",
"in-bookmarks": "In bookmarks",
"in-categories": "Trong danh mục",
"in-users": "Trong người dùng",
"in-tags": "Trong thẻ",

View File

@@ -7,6 +7,7 @@
"in-titles": "在标题中",
"in-titles-posts": "在标题和帖子中",
"in-posts": "在帖子中",
"in-bookmarks": "In bookmarks",
"in-categories": "在板块中",
"in-users": "在用户中",
"in-tags": "在标签中",

View File

@@ -7,6 +7,7 @@
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In posts",
"in-bookmarks": "In bookmarks",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",

View File

@@ -51,18 +51,6 @@
}
}
.btn-outline {
@include btn-ghost-base();
border-color: $border-color;
}
.btn-outline-sm {
@include btn-ghost-base();
border-color: $border-color;
font-size: 0.875rem;
line-height: 1.25rem;
}
@include color-mode(dark) {
.btn-light {
@extend .btn-dark;
@@ -70,7 +58,7 @@
.text-bg-light {
@extend .text-bg-dark;
}
.btn-ghost, .btn-ghost-sm, .btn-outline, .btn-outline-sm {
.btn-ghost, .btn-ghost-sm {
color: $btn-ghost-color-dark;
&:hover, &.active {
background-color: $btn-ghost-hover-color-dark;

View File

@@ -0,0 +1,51 @@
$btn-ghost-hover-color: mix($light, $dark, 90%)!default;
$btn-ghost-active-color: lighten($btn-ghost-hover-color, 5%)!default;
$btn-ghost-hover-color-dark: mix($dark, $light, 90%)!default;
$btn-ghost-active-color-dark: lighten($btn-ghost-hover-color-dark, 5%)!default;
:root {
--btn-ghost-hover-color: #{$btn-ghost-hover-color};
--btn-ghost-active-color: #{$btn-ghost-active-color};
}
[data-bs-theme="dark"] {
--btn-ghost-hover-color: #{$btn-ghost-hover-color-dark};
--btn-ghost-active-color: #{$btn-ghost-active-color-dark};
}
@mixin btn-ghost-base {
display: flex;
align-items: center;
justify-content: center;
gap: ($spacer * 0.5);
border-radius: $border-radius-sm;
border-width: 1px;
border-color: transparent;
background-color: transparent;
box-shadow: none;
padding: ($spacer * 0.25) ($spacer * 0.5);
text-align: left;
--bs-text-opacity: 1;
color: inherit !important;
cursor: pointer;
&:hover, &.active {
background-color: var(--btn-ghost-hover-color);
text-decoration: none;
}
}
.btn-ghost {
@include btn-ghost-base();
line-height: 1.5rem;
> i {
line-height: 1.5rem;
}
}
.btn-ghost-sm {
@include btn-ghost-base();
font-size: 0.875rem;
line-height: 1.25rem;
> i {
line-height: 1.25rem;
}
}

127
public/scss/chats.scss Normal file
View File

@@ -0,0 +1,127 @@
// chats need a bit of css
.stacked-avatars {
width: 32px;
height: 32px;
span:first-child {
top: 0;
left: 8px;
}
span:last-child {
left: 0;
top: 8px;
}
}
body.page-user-chats {
#content {
max-width: 100%;
margin-bottom: 0!important;
}
overflow: hidden;
[data-widget-area="footer"] {
display: none;
}
height: 100%;
}
[component="chat/recent"] {
.active .chat-room-btn {
background-color: var(--btn-ghost-hover-color);
}
}
[component="chat/nav-wrapper"] {
width: 300px;
[component="chat/public/room"].unread {
font-weight: $font-weight-bold;
}
}
[component="chat/user/list"] [data-uid] {
[component="chat/user/list/username"] {
color: $text-muted;
}
&.online {
[component="chat/user/list/username"] {
color: initial;
font-weight: $font-weight-semibold;
}
}
}
.expanded-chat {
.chat-content {
.message-body {
@include fix-lists;
}
.chat-message {
.message-body-wrapper {
.controls {
opacity: 0;
transition: $transition-fade;
&:has([aria-expanded="true"]) { opacity: 1; }
[data-action="restore"], [data-action="unpin"] { display: none; }
}
&:hover {
.controls { opacity: 1; }
}
}
&.deleted {
.message-body { opacity: 0.3; }
.message-body-wrapper .controls {
[data-action] { display: none; }
[data-action="restore"] { display: block; }
}
}
&.pinned {
.message-body-wrapper .controls {
[data-action="pin"] { display: none; }
[data-action="unpin"] { display: block;}
}
}
}
}
}
/* Mobile handling of chat page */
@include media-breakpoint-down(lg) {
.page-user-chats.chat-loaded {
padding-bottom: 4.75rem;
}
}
@include media-breakpoint-down(md) {
.page-user-chats.chat-loaded {
padding-bottom: initial;
}
[component="chat/nav-wrapper"] {
width: 100%;
}
.page-user-chats.chat-loaded .bottombar {
display: none!important;
}
[component="chat/nav-wrapper"][data-loaded="1"] {
display: none!important;
}
[component="chat/nav-wrapper"][data-loaded="0"] + [component="chat/main-wrapper"] {
display: none!important;
}
}
.chat-modal {
left: auto;
top: auto;
bottom: 0px;
right: 2rem;
width: auto!important;
height: auto!important;
[component="chat/user/list/btn"], [component="chat/pinned/messages/btn"] {
display: none!important;
}
}

View File

@@ -1,7 +1,9 @@
// core scss files shared by all themes
@import "flags";
@import "chats";
@import "global";
@import "modals";
@import "btn-ghost";
@import "modules/picture-switcher";
@import "modules/bottom-sheet";
@import "modules/icon-picker";

View File

@@ -35,7 +35,7 @@ define('forum/chats/recent', ['alerts', 'api', 'chat'], function (alerts, api, c
return;
}
recentChats.attr('loading', 1);
app.get(`/chats`, {
api.get(`/chats`, {
uid: ajaxify.data.uid,
after: recentChats.attr('data-nextstart'),
}).then(({ rooms, nextStart }) => {

View File

@@ -119,7 +119,7 @@ define('forum/search', [
in: $('#search-in').val(),
};
searchData.term = $('#search-input').val();
if (searchData.in === 'posts' || searchData.in === 'titlesposts' || searchData.in === 'titles') {
if (['posts', 'titlesposts', 'titles', 'bookmarks'].includes(searchData.in)) {
searchData.matchWords = form.find('#match-words-filter').val();
searchData.by = selectedUsers.length ? selectedUsers.map(u => u.username) : undefined;
searchData.categories = selectedCids.length ? selectedCids : undefined;
@@ -143,7 +143,7 @@ define('forum/search', [
}
function updateFormItemVisiblity(searchIn) {
const hideTitlePostFilters = !searchIn.includes('posts') && !searchIn.includes('titles');
const hideTitlePostFilters = !['posts', 'titles', 'bookmarks'].some(token => searchIn.includes(token));
$('.post-search-item').toggleClass('hidden', hideTitlePostFilters);
}
@@ -252,6 +252,7 @@ define('forum/search', [
function categoryFilterDropdown(_selectedCids) {
ajaxify.data.allCategoriesUrl = '';
selectedCids = _selectedCids || [];
const dropdownEl = $('[component="category/filter"]');
categoryFilter.init(dropdownEl, {
selectedCids: _selectedCids,
@@ -290,6 +291,7 @@ define('forum/search', [
}
function userFilterDropdown(el, _selectedUsers) {
selectedUsers = _selectedUsers || [];
userFilter.init(el, {
selectedUsers: _selectedUsers,
template: 'partials/search-filters',

View File

@@ -36,7 +36,7 @@ searchController.search = async function (req, res, next) {
let allowed = (req.query.in === 'users' && userPrivileges['search:users']) ||
(req.query.in === 'tags' && userPrivileges['search:tags']) ||
(req.query.in === 'categories') ||
(['titles', 'titlesposts', 'posts'].includes(req.query.in) && userPrivileges['search:content']);
(['titles', 'titlesposts', 'posts', 'bookmarks'].includes(req.query.in) && userPrivileges['search:content']);
({ allowed } = await plugins.hooks.fire('filter:search.isAllowed', {
uid: req.uid,
query: req.query,

View File

@@ -261,4 +261,21 @@ module.exports = function (Posts) {
}
});
}
Posts.filterPidsByUid = async function (pids, uids) {
if (!uids) {
return pids;
}
if (!Array.isArray(uids) || uids.length === 1) {
return await filterPidsBySingleUid(pids, uids);
}
const pidsArr = await Promise.all(uids.map(uid => Posts.filterPidsByUid(pids, uid)));
return _.union(...pidsArr);
};
async function filterPidsBySingleUid(pids, uid) {
const isMembers = await db.isSortedSetMembers(`uid:${parseInt(uid, 10)}:posts`, pids);
return pids.filter((pid, index) => pid && isMembers[index]);
}
};

View File

@@ -3,6 +3,7 @@
const _ = require('lodash');
const db = require('./database');
const batch = require('./batch');
const posts = require('./posts');
const topics = require('./topics');
const categories = require('./categories');
@@ -18,7 +19,7 @@ search.search = async function (data) {
data.sortBy = data.sortBy || 'relevance';
let result;
if (data.searchIn === 'posts' || data.searchIn === 'titles' || data.searchIn === 'titlesposts') {
if (['posts', 'titles', 'titlesposts', 'bookmarks'].includes(data.searchIn)) {
result = await searchInContent(data);
} else if (data.searchIn === 'users') {
result = await user.search(data);
@@ -68,6 +69,8 @@ async function searchInContent(data) {
const tid = inTopic[1];
const cleanedTerm = data.query.replace(inTopic[0], '');
pids = await topics.search(tid, cleanedTerm);
} else if (data.searchIn === 'bookmarks') {
pids = await searchInBookmarks(data, searchCids, searchUids);
} else {
[pids, tids] = await Promise.all([
doSearch('post', ['posts', 'titlesposts']),
@@ -115,8 +118,46 @@ async function searchInContent(data) {
return Object.assign(returnData, metadata);
}
async function searchInBookmarks(data, searchCids, searchUids) {
const { uid, query, matchWords } = data;
const allPids = [];
await batch.processSortedSet(`uid:${uid}:bookmarks`, async (pids) => {
if (Array.isArray(searchCids) && searchCids.length) {
pids = await posts.filterPidsByCid(pids, searchCids);
}
if (Array.isArray(searchUids) && searchUids.length) {
pids = await posts.filterPidsByUid(pids, searchUids);
}
if (query) {
const tokens = String(query).split(' ');
const postData = await db.getObjectsFields(pids.map(pid => `post:${pid}`), ['content', 'tid']);
const tids = _.uniq(postData.map(p => p.tid));
const topicData = await db.getObjectsFields(tids.map(tid => `topic:${tid}`), ['title']);
const tidToTopic = _.zipObject(tids, topicData);
pids = pids.filter((pid, i) => {
const content = String(postData[i].content);
const title = String(tidToTopic[postData[i].tid].title);
const method = (matchWords === 'any' ? 'some' : 'every');
return tokens[method](
token => content.includes(token) || title.includes(token)
);
});
}
allPids.push(...pids);
}, {
batch: 500,
});
return allPids;
}
async function filterAndSort(pids, data) {
if (data.sortBy === 'relevance' && !data.replies && !data.timeRange && !data.hasTags && !plugins.hooks.hasListeners('filter:search.filterAndSort')) {
if (data.sortBy === 'relevance' &&
!data.replies &&
!data.timeRange &&
!data.hasTags &&
data.searchIn !== 'bookmarks' &&
!plugins.hooks.hasListeners('filter:search.filterAndSort')) {
return pids;
}
let postsData = await getMatchedPosts(pids, data);

View File

@@ -30,43 +30,43 @@ const Events = module.exports;
Events._types = {
pin: {
icon: 'fa-thumb-tack',
translation: async event => translateSimple(event, 'topic:user-pinned-topic'),
translation: async (event, language) => translateSimple(event, language, 'topic:user-pinned-topic'),
},
unpin: {
icon: 'fa-thumb-tack fa-rotate-90',
translation: async event => translateSimple(event, 'topic:user-unpinned-topic'),
translation: async (event, language) => translateSimple(event, language, 'topic:user-unpinned-topic'),
},
lock: {
icon: 'fa-lock',
translation: async event => translateSimple(event, 'topic:user-locked-topic'),
translation: async (event, language) => translateSimple(event, language, 'topic:user-locked-topic'),
},
unlock: {
icon: 'fa-unlock',
translation: async event => translateSimple(event, 'topic:user-unlocked-topic'),
translation: async (event, language) => translateSimple(event, language, 'topic:user-unlocked-topic'),
},
delete: {
icon: 'fa-trash',
translation: async event => translateSimple(event, 'topic:user-deleted-topic'),
translation: async (event, language) => translateSimple(event, language, 'topic:user-deleted-topic'),
},
restore: {
icon: 'fa-trash-o',
translation: async event => translateSimple(event, 'topic:user-restored-topic'),
translation: async (event, language) => translateSimple(event, language, 'topic:user-restored-topic'),
},
move: {
icon: 'fa-arrow-circle-right',
translation: async event => translateEventArgs(event, 'topic:user-moved-topic-from', renderUser(event), `${event.fromCategory.name}`, renderTimeago(event)),
translation: async (event, language) => translateEventArgs(event, language, 'topic:user-moved-topic-from', renderUser(event), `${event.fromCategory.name}`, renderTimeago(event)),
},
'post-queue': {
icon: 'fa-history',
translation: async event => translateEventArgs(event, 'topic:user-queued-post', renderUser(event), `${relative_path}${event.href}`, renderTimeago(event)),
translation: async (event, language) => translateEventArgs(event, language, 'topic:user-queued-post', renderUser(event), `${relative_path}${event.href}`, renderTimeago(event)),
},
backlink: {
icon: 'fa-link',
translation: async event => translateEventArgs(event, 'topic:user-referenced-topic', renderUser(event), `${relative_path}${event.href}`, renderTimeago(event)),
translation: async (event, language) => translateEventArgs(event, language, 'topic:user-referenced-topic', renderUser(event), `${relative_path}${event.href}`, renderTimeago(event)),
},
fork: {
icon: 'fa-code-fork',
translation: async event => translateEventArgs(event, 'topic:user-forked-topic', renderUser(event), `${relative_path}${event.href}`, renderTimeago(event)),
translation: async (event, language) => translateEventArgs(event, language, 'topic:user-forked-topic', renderUser(event), `${relative_path}${event.href}`, renderTimeago(event)),
},
};
@@ -76,14 +76,14 @@ Events.init = async () => {
Events._types = types;
};
async function translateEventArgs(event, prefix, ...args) {
async function translateEventArgs(event, language, prefix, ...args) {
const key = getTranslationKey(event, prefix);
const compiled = translator.compile.apply(null, [key, ...args]);
return utils.decodeHTMLEntities(await translator.translate(compiled));
return utils.decodeHTMLEntities(await translator.translate(compiled, language));
}
async function translateSimple(event, prefix) {
return await translateEventArgs(event, prefix, renderUser(event), renderTimeago(event));
async function translateSimple(event, language, prefix) {
return await translateEventArgs(event, language, prefix, renderUser(event), renderTimeago(event));
}
Events.translateSimple = translateSimple; // so plugins can perform translate
@@ -162,9 +162,10 @@ async function modifyEvent({ tid, uid, eventIds, timestamps, events }) {
});
}
const [users, fromCategories] = await Promise.all([
const [users, fromCategories, userSettings] = await Promise.all([
getUserInfo(events.map(event => event.uid).filter(Boolean)),
getCategoryInfo(events.map(event => event.fromCid).filter(Boolean)),
user.getSettings(uid),
]);
// Remove backlink events if backlinks are disabled
@@ -200,7 +201,7 @@ async function modifyEvent({ tid, uid, eventIds, timestamps, events }) {
await Promise.all(events.map(async (event) => {
if (Events._types[event.type].translation) {
event.text = await Events._types[event.type].translation(event);
event.text = await Events._types[event.type].translation(event, userSettings.userLang);
}
}));

37
src/views/chat.tpl Normal file
View File

@@ -0,0 +1,37 @@
<div id="chat-modal" class="chat-modal d-flex flex-nowrap modal hide overflow-visible" tabindex="-1" role="dialog" aria-labelledby="Chat" aria-hidden="true" data-center="false">
<div class="modal-dialog">
<div class="modal-content" component="chat/message/window">
<div class="modal-header d-flex gap-4 justify-content-between">
<div class="fs-6 flex-grow-1 fw-semibold tracking-tight text-truncate text-nowrap" component="chat/room/name" data-icon="{icon}">{{{ if ./roomName }}}<i class="fa {icon} text-muted"></i> {roomName}{{{ else }}}{./chatWithMessage}{{{ end}}}</div>
<div class="d-flex gap-1 align-items-center">
<button type="button" class="btn-ghost-sm d-none d-md-flex" data-action="maximize" title="[[modules:chat.maximize]]" data-bs-toggle="tooltip" data-bs-placement="bottom">
<i class="fa fa-fw fa-expand text-muted"></i>
</button>
<!-- IMPORT partials/chats/options.tpl -->
<button id="chat-close-btn" type="button" class="btn-close btn-ghost-sm" aria-label="Close"></button>
</div>
</div>
<!-- IMPORT partials/chats/scroll-up-alert.tpl -->
<div class="modal-body d-flex flex-column" style="height: 500px;">
<div class="d-flex flex-grow-1 gap-1 overflow-auto" style="min-width: 0px;">
<div component="chat/messages" class="expanded-chat d-flex flex-column flex-grow-1" data-roomid="{roomId}" style="min-width: 0px;">
<ul component="chat/message/content" class="chat-content p-0 m-0 list-unstyled overflow-auto flex-grow-1">
<!-- IMPORT partials/chats/messages.tpl -->
</ul>
<ul component="chat/message/search/results" class="chat-content p-0 m-0 list-unstyled overflow-auto flex-grow-1 hidden">
<div component="chat/message/search/no-results" class="text-center p-4 d-flex flex-column">
<div class="p-4"><i class="fa-solid fa-wind fs-2 text-muted"></i></div>
<div class="text-xs fw-semibold text-muted">[[search:no-matches]]</div>
</div>
</ul>
<!-- IMPORT partials/chats/composer.tpl -->
</div>
</div>
</div>
<div class="imagedrop"><div>[[topic:composer.drag-and-drop-images]]</div></div>
</div>
</div>
</div>

54
src/views/chats.tpl Normal file
View File

@@ -0,0 +1,54 @@
<div class="chats-full d-flex gap-1 h-100 mt-3 mt-md-0 py-md-3">
<div component="chat/nav-wrapper" class="flex-shrink-0 d-flex flex-column h-100 gap-1" data-loaded="{{{ if roomId }}}1{{{ else }}}0{{{ end }}}">
<div>
<button component="chat/create" class="btn btn-primary btn-sm w-100">[[modules:chat.create-room]]</button>
</div>
{{{ if publicRooms.length }}}
<hr class="my-1">
<div class="d-flex flex-column gap-1">
<div class="d-flex gap-1 align-items-center justify-content-between justify-content-lg-start">
<button class="btn-ghost-sm p-1 order-1 order-lg-0" data-bs-toggle="collapse" data-bs-target="#public-rooms"
onclick="$(this).find('i').toggleClass('fa-rotate-180');"><i class="fa fa-fw fa-chevron-up" style="transition: 0.25s ease;"></i></button>
<label class="text-sm text-muted lh-1">[[modules:chat.public-rooms, {publicRooms.length}]]</label>
</div>
<div id="public-rooms" component="chat/public" class="collapse show">
<div class="d-flex gap-1 flex-column">
{{{ each publicRooms }}}
<div component="chat/public/room" class="btn-ghost-sm ff-sans justify-content-between hover-parent {{{ if ./unread}}}unread{{{ end }}}" data-roomid="{./roomId}">
<div class="d-flex gap-1 align-items-center"><i class="fa {./icon} text-muted"></i> {./roomName} <div component="chat/public/room/unread/count" data-count="{./unreadCount}" class="badge border bg-light text-primary {{{ if !./unreadCount }}}hidden{{{ end }}}">{./unreadCountText}</div></div>
<div>
<div component="chat/public/room/sort/handle" class="text-muted {{{ if isAdmin }}}hover-d-block{{{ else }}}d-none{{{ end }}}" style="cursor:grab;"><i class="fa fa-bars"></i></div>
</div>
</div>
{{{ end }}}
</div>
</div>
</div>
{{{ end }}}
<hr class="my-1">
<div class="d-flex flex-column gap-1 overflow-auto">
{{{ if publicRooms.length }}}
<div class="d-flex gap-1 align-items-center justify-content-between justify-content-lg-start">
<button class="btn-ghost-sm p-1 order-1 order-lg-0" data-bs-toggle="collapse" data-bs-target="#private-rooms"
onclick="$(this).find('i').toggleClass('fa-rotate-180')"><i class="fa fa-fw fa-chevron-up" style="transition: 0.25s ease;"></i></button>
<label class="text-sm text-muted lh-1">[[modules:chat.private-rooms, {privateRoomCount}]]</label>
</div>
{{{ end }}}
<div id="private-rooms" component="chat/recent" class="chats-list overflow-auto mb-0 pe-1 pb-5 pb-lg-0 collapse show" data-nextstart="{nextStart}">
{{{each rooms}}}
<!-- IMPORT partials/chats/recent_room.tpl -->
{{{end}}}
</div>
</div>
</div>
<div component="chat/main-wrapper" class="flex-grow-1 ms-md-2 ps-md-2 border-1 border-start-md h-100" style="min-width: 0;" data-roomid="{roomId}">
<!-- IMPORT partials/chats/message-window.tpl -->
</div>
<div class="imagedrop"><div>[[topic:composer.drag-and-drop-images]]</div></div>
</div>

View File

@@ -0,0 +1 @@
<!-- This partial intentionally left blank; overwritten by nodebb-plugin-reactions -->

View File

@@ -0,0 +1,27 @@
<div component="chat/composer" class="d-flex flex-column border-top pt-2 align-items-start">
<div component="chat/composer/replying-to" data-tomid="" class="text-sm px-2 mb-1 d-flex gap-2 align-items-center hidden">
<div component="chat/composer/replying-to-text"></div> <button component="chat/composer/replying-to-cancel" class="btn-ghost-sm px-2 py-1"><i class="fa fa-times"></i></button>
</div>
<div class="w-100 flex-grow-1 flex-nowrap position-relative d-flex rounded-2 border border-secondary p-1 align-items-end">
<button component="chat/upload/button" class="btn-ghost-sm px-2" type="button" title="[[global:upload]]" data-bs-toggle="tooltip"><i class="fa fa-fw fa-upload"></i></button>
<div class="flex-grow-1 align-self-center">
<textarea component="chat/input" placeholder="[[modules:chat.placeholder.mobile]]" class="bg-transparent text-body form-control chat-input mousetrap rounded-0 border-0 shadow-none px-1 py-0" style="min-height: 1.5rem;height:0;max-height:30vh;resize:none;"></textarea>
</div>
<div class="d-flex gap-1">
{{{ each composerActions }}}
<button data-action="{./action}" class="btn-ghost-sm px-2 {./class}" type="button" title="{./title}" data-bs-toggle="tooltip"><i class="fa {./icon}"></i></button>
{{{ end }}}
<button class="btn-ghost-sm px-2" type="button" data-action="send" title="[[modules:chat.send]]" data-bs-toggle="tooltip"><i class="fa fa-fw fa-paper-plane text-primary"></i></button>
</div>
</div>
<div class="d-flex justify-content-between align-items-center text-xs w-100 px-2 mt-1">
<div component="chat/composer/typing" class="">
<div component="chat/composer/typing/users" class="hidden"></div>
<div component="chat/composer/typing/text" class="hidden"></div>
</div>
<div component="chat/message/remaining" class="text-xs text-muted">{maximumChatMessageLength}</div>
</div>
<form class="hidden" component="chat/upload" method="post" enctype="multipart/form-data">
<input type="file" name="files[]" multiple class="hidden"/>
</form>
</div>

View File

@@ -0,0 +1,10 @@
{{{ if !rooms.length }}}
<li class="text-center p-4 d-flex flex-column">
<div class="p-4"><i class="fa-solid fa-wind fs-2 text-muted"></i></div>
<div class="text-xs fw-semibold text-muted">[[modules:chat.no-active]]</div>
</li>
{{{ end }}}
{{{ each rooms }}}
<!-- IMPORT partials/chats/recent_room.tpl -->
{{{ end }}}

View File

@@ -0,0 +1,43 @@
<div component="chat/message/window" class="d-flex flex-column h-100">
{{{ if widgets.header.length }}}
<div data-widget-area="header">
{{{each widgets.header}}}
{{widgets.header.html}}
{{{end}}}
</div>
{{{ end }}}
{{{ if !roomId }}}
<div class="d-flex flex-column align-items-center gap-3 my-auto">
<i class="fa-solid fa-wind fs-2 text-muted"></i>
<span class="text-muted text-sm">[[modules:chat.no-active]]</span>
</div>
{{{ else }}}
<div component="chat/header" class="d-flex align-items-center px-md-3 gap-3">
<a href="#" data-action="close" role="button" class="flex-shrink-0 d-flex d-md-none btn btn-ghost border align-text-top"><i class="fa fa-chevron-left"></i></a>
<h5 component="chat/header/title" class="members flex-grow-1 fw-semibold tracking-tight mb-0 text-truncate text-nowrap">
{{{ if ./roomName }}}<i class="fa {icon} text-muted"></i> {roomName}{{{ else }}}{./chatWithMessage}{{{ end}}}
</h5>
<!-- IMPORT partials/chats/options.tpl -->
</div>
<!-- IMPORT partials/chats/scroll-up-alert.tpl -->
<hr class="my-1"/>
<div class="d-flex flex-grow-1 gap-1 overflow-auto" style="min-width: 0px;">
<div component="chat/messages" class="expanded-chat d-flex flex-column flex-grow-1" data-roomid="{roomId}" style="min-width: 0px;">
<ul component="chat/message/content" class="chat-content p-0 m-0 list-unstyled overflow-auto flex-grow-1">
<!-- IMPORT partials/chats/messages.tpl -->
</ul>
<ul component="chat/message/search/results" class="chat-content p-0 m-0 list-unstyled overflow-auto flex-grow-1 hidden">
<div component="chat/message/search/no-results" class="text-center p-4 d-flex flex-column">
<div class="p-4"><i class="fa-solid fa-wind fs-2 text-muted"></i></div>
<div class="text-xs fw-semibold text-muted">[[search:no-matches]]</div>
</div>
</ul>
<!-- IMPORT partials/chats/composer.tpl -->
</div>
<!-- IMPORT partials/chats/user-list.tpl -->
<!-- IMPORT partials/chats/pinned-messages.tpl -->
</div>
{{{ end }}}
</div>

View File

@@ -0,0 +1,73 @@
<li component="chat/message" class="chat-message mx-2 pe-2 {{{ if messages.deleted }}} deleted{{{ end }}} {{{ if messages.pinned}}} pinned{{{ end }}} {{{ if messages.newSet }}}border-top pt-3{{{ end }}}" data-mid="{messages.messageId}" data-uid="{messages.fromuid}" data-index="{messages.index}" data-self="{messages.self}" data-break="{messages.newSet}" data-timestamp="{messages.timestamp}" data-username="{messages.fromUser.username}">
{{{ if messages.parent }}}
<!-- IMPORT partials/chats/parent.tpl -->
{{{ end }}}
<div class="message-header lh-1 d-flex align-items-center gap-2 text-sm {{{ if !messages.newSet }}}hidden{{{ end }}} pb-2">
<a href="{config.relative_path}/user/{messages.fromUser.userslug}" class="text-decoration-none">{buildAvatar(messages.fromUser, "18px", true, "not-responsive")}</a>
<span class="chat-user fw-semibold"><a href="{config.relative_path}/user/{messages.fromUser.userslug}">{messages.fromUser.displayname}</a></span>
{{{ if messages.fromUser.banned }}}
<span class="badge bg-danger">[[user:banned]]</span>
{{{ end }}}
{{{ if messages.fromUser.deleted }}}
<span class="badge bg-danger">[[user:deleted]]</span>
{{{ end }}}
<span class="chat-timestamp text-muted timeago" title="{messages.timestampISO}"></span>
<div component="chat/message/edited" class="text-muted ms-auto {{{ if !messages.edited }}}hidden{{{ end }}}" title="[[global:edited-timestamp, {isoTimeToLocaleString(messages.editedISO, config.userLang)}]]"><i class="fa fa-edit"></i></span></div>
</div>
<div class="message-body-wrapper">
<div component="chat/message/body" class="message-body ps-0 py-0 overflow-auto text-break">
{messages.content}
</div>
<!-- IMPORT partials/chats/reactions.tpl -->
<div component="chat/message/controls" class="position-relative">
<div class="btn-group border shadow-sm controls position-absolute bg-body end-0" style="bottom:1rem;">
<!-- IMPORT partials/chats/add-reaction.tpl -->
<button class="btn btn-sm btn-link" data-action="reply" title="[[topic:reply]]"><i class="fa fa-reply"></i></button>
<div class="btn-group d-inline-block">
<button class="btn btn-sm btn-link dropdown-toggle" data-bs-toggle="dropdown"><i class="fa fa-ellipsis" type="button"></i></button>
<ul class="dropdown-menu dropdown-menu-end p-1 text-sm list-unstyled">
{{{ if (isAdminOrGlobalMod || (!config.disableChatMessageEditing && messages.self)) }}}
<li>
<a href="#" class="dropdown-item rounded-1" data-action="edit"><span class="d-inline-flex align-items-center gap-2"><i class="fa fa-fw fa-pencil text-muted"></i> [[topic:edit]]</span></a>
</li>
<li>
<a href="#" class="dropdown-item rounded-1" data-action="delete"><span class="d-inline-flex align-items-center gap-2"><i class="fa fa-fw fa-trash text-muted"></i> [[topic:delete]]</span></a>
</li>
<li>
<a href="#" class="dropdown-item rounded-1" data-action="restore"><span class="d-inline-flex align-items-center gap-2"><i class="fa fa-fw fa-repeat text-muted"></i> [[topic:restore]]</span></a>
</li>
{{{ end }}}
{{{ if (isAdminOrGlobalMod || isOwner )}}}
<li>
<a href="#" class="dropdown-item rounded-1" data-action="pin"><span class="d-inline-flex align-items-center gap-2"><i class="fa fa-fw fa-thumbtack text-muted"></i> [[modules:chat.pin-message]]</span></a>
</li>
<li>
<a href="#" class="dropdown-item rounded-1" data-action="unpin"><span class="d-inline-flex align-items-center gap-2"><i class="fa fa-fw fa-thumbtack fa-rotate-90 text-muted"></i> [[modules:chat.unpin-message]]</span></a>
</li>
<li class="dropdown-divider"></li>
{{{ end }}}
{{{ if isAdminOrGlobalMod }}}
<li>
<a href="#" class="dropdown-item rounded-1 chat-ip-button"><span class="d-inline-flex align-items-center gap-2"><i class="fa fa-fw fa-info-circle text-muted"></i> [[modules:chat.show-ip]]</span></a>
</li>
{{{ end }}}
<li>
<a href="#" class="dropdown-item rounded-1" data-action="copy-text" data-mid="{messages.mid}"><span class="d-inline-flex align-items-center gap-2"><i class="fa fa-fw fa-copy text-muted"></i> [[modules:chat.copy-text]]</span></a>
</li>
<li>
<a href="#" class="dropdown-item rounded-1" data-action="copy-link" data-mid="{messages.mid}"><span class="d-inline-flex align-items-center gap-2"><i class="fa fa-fw fa-link text-muted"></i> [[modules:chat.copy-link]]</span></a>
</li>
</ul>
</div>
</div>
</div>
</div>
</li>

View File

@@ -0,0 +1,7 @@
{{{each messages}}}
{{{ if !./system }}}
<!-- IMPORT partials/chats/message.tpl -->
{{{ else }}}
<!-- IMPORT partials/chats/system-message.tpl -->
{{{ end }}}
{{{end}}}

View File

@@ -0,0 +1,100 @@
<div class="d-flex gap-1 align-items-stretch">
<!-- search -->
<button class="btn-ghost-sm" component="chat/room/search/toggle" data-manual-tooltip="1" title="[[global:header.search]]">
<i class="fa fa-search text-muted"></i>
</button>
<div component="chat/room/search/container" class="position-relative hidden align-self-center">
<input component="chat/room/search" class="form-control form-control-sm" placeholder="[[search:type-to-search]]" style="width: 150px;">
<a component="chat/room/search/clear" href="#" class="hidden px-2 py-1 position-absolute top-50 end-0 translate-middle-y">
<i class="fa fa-times text-muted opacity-75"></i>
</a>
</div>
<!-- notification dropdown -->
<div class="dropdown d-flex" data-manual-tooltip="1" title="[[modules:chat.notification-settings]]" component="chat/notification/setting">
<button class="btn-ghost-sm position-relative" data-bs-toggle="dropdown">
<i class="fa fa-bell text-muted"></i>
<span class="position-absolute top-0 end-0 text-xs text-muted opacity-50" style="font-size: 10px!important; padding: 1px; line-height: 10px;">
<i component="chat/notification/setting/icon" class="fa {notificationOptionsIcon}"></i>
</span>
</button>
<ul class="dropdown-menu dropdown-menu-end p-1 text-sm">
{{{ each notificationOptions }}}
<li>
<a class="dropdown-item rounded-1 d-flex align-items-center gap-2" href="#" data-value="{./value}" data-icon="{./icon}">
<div class="d-flex flex-column gap-1">
<div class="d-flex align-items-center gap-2">
<div class="flex-grow-1">{./label}</div>
<i class="flex-shrink-0 fa fa-fw fa-check {{{ if !./selected }}}hidden{{{ end }}}"></i>
</div>
{{{ if @first}}}
<div component="chat/notification/setting/sub-label" class="text-sm text-muted">{./subLabel}</div>
{{{ end }}}
</div>
</a>
</li>
{{{ if @first }}}
<li><hr class="dropdown-divider"></li>
{{{ end }}}
{{{ end }}}
</ul>
</div>
<!-- pinned messages -->
<button component="chat/pinned/messages/btn" class="btn-ghost-sm d-none d-lg-flex flex-nowrap" title="[[modules:chat.pinned-messages]]" data-bs-toggle="tooltip" data-bs-placement="bottom">
<i class="fa fa-thumb-tack text-muted"></i>
</button>
<!-- manage/options dropdown -->
<div class="dropdown d-flex" data-manual-tooltip="1" title="[[modules:chat.options]]">
<button class="btn-ghost-sm" data-bs-toggle="dropdown" component="chat/controlsToggle">
<i class="fa fa-gear text-muted"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end p-1 text-sm" component="chat/controls">
<li>
<a class="dropdown-item rounded-1 d-flex align-items-center gap-2" href="#" data-action="manage">
<i class="fa fa-fw text-muted fa-cog"></i> [[modules:chat.manage-room]]
</a>
</li>
{{{ if isOwner }}}
<li>
<a class="dropdown-item rounded-1 d-flex align-items-center gap-2" href="#" data-action="rename">
<i class="fa fa-fw text-muted fa-edit"></i> [[modules:chat.rename-room]]
</a>
</li>
{{{ end }}}
<li>
<a class="dropdown-item rounded-1 d-flex align-items-center gap-2" href="#" data-action="leave">
<i class="fa fa-fw text-muted fa-sign-out"></i> [[modules:chat.leave-room]]
</a>
</li>
{{{ if (public && isAdmin) }}}
<li>
<a class="dropdown-item rounded-1 d-flex align-items-center gap-2" href="#" data-action="delete">
<i class="fa fa-fw text-danger fa-trash"></i> [[modules:chat.delete-room]]
</a>
</li>
{{{ end }}}
</ul>
</div>
<!-- users toggle -->
{{{ if users.length }}}
<div component="chat/user/list/btn" class="btn-ghost-sm d-none d-lg-flex flex-nowrap gap-3" title="[[modules:chat.view-users-list]]" data-bs-toggle="tooltip" data-bs-placement="bottom">
<div class="d-flex text-nowrap">
{{{ if ./users.0 }}}
<span style="width: 18px; z-index: 3;" class="text-decoration-none" href="{config.relative_path}/user/{./users.0.userslug}">{buildAvatar(./users.0, "24px", true)}</span>
{{{ end }}}
{{{ if ./users.1 }}}
<span style="width: 18px; z-index: 2;" class="text-decoration-none" href="{config.relative_path}/user/{./users.1.userslug}">{buildAvatar(./users.1, "24px", true)}</span>
{{{ end }}}
{{{ if ./users.2 }}}
<span style="width: 18px; z-index: 1;" class="text-decoration-none" href="{config.relative_path}/user/{./users.2.userslug}">{buildAvatar(./users.2, "24px", true)}</span>
{{{ end }}}
</div>
{./userCount}
</div>
{{{ end }}}
</div>

View File

@@ -0,0 +1,13 @@
<div class="d-flex ms-4 mb-2 align-items-center">
<div component="chat/message/parent" data-parent-mid="{messages.parent.mid}" data-uid="{messages.parent.fromuid}" class="btn-ghost-sm ff-secondary align-items-start flex-row w-100">
<div class="d-flex gap-2 text-sm text-nowrap">
<div><i class="fa fa-sm fa-reply opacity-50"></i></div>
<div class="d-flex flex-nowrap gap-1 align-items-center">
<a href="{config.relative_path}/user/{messages.parent.user.userslug}" class="text-decoration-none lh-1">{buildAvatar(messages.parent.user, "14px", true, "not-responsive align-middle")}</a>
<a class="chat-user fw-semibold" href="{config.relative_path}/user/{messages.parent.user.userslug}">{messages.parent.user.displayname}</a>
</div>
<span class="chat-timestamp text-muted timeago text-nowrap hidden" title="{messages.parent.timestampISO}"></span>
</div>
<div component="chat/message/parent/content" class="text-muted line-clamp-1 w-100">{messages.parent.content}</div>
</div>
</div>

View File

@@ -0,0 +1,32 @@
{{{ each messages }}}
<li component="chat/message" class="chat-message mx-2 pe-2 {{{ if messages.deleted }}} deleted{{{ end }}} {{{ if messages.pinned}}} pinned{{{ end }}} border-top pt-3" data-mid="{messages.messageId}" data-uid="{messages.fromuid}" data-self="{messages.self}" data-timestamp="{messages.timestamp}" data-username="{messages.fromUser.username}" data-index="{./index}">
{{{ if messages.parent }}}
<!-- IMPORT partials/chats/parent.tpl -->
{{{ end }}}
<div class="message-header lh-1 d-flex align-items-center gap-2 text-sm pb-2">
<a href="{config.relative_path}/user/{messages.fromUser.userslug}" class="text-decoration-none">{buildAvatar(messages.fromUser, "18px", true, "not-responsive")}</a>
<span class="chat-user fw-semibold"><a href="{config.relative_path}/user/{messages.fromUser.userslug}">{messages.fromUser.displayname}</a></span>
<span class="chat-timestamp text-muted timeago" title="{messages.timestampISO}"></span>
<div component="chat/message/edited" class="text-muted ms-auto {{{ if !messages.edited }}}hidden{{{ end }}}" title="[[global:edited-timestamp, {isoTimeToLocaleString(messages.editedISO, config.userLang)}]]"><i class="fa fa-edit"></i></span></div>
</div>
<div class="message-body-wrapper">
<div component="chat/message/body" class="message-body ps-0 py-0 overflow-auto text-break">
{messages.content}
</div>
<div component="chat/message/controls" class="position-relative">
<div class="btn-group border shadow-sm controls position-absolute bg-body end-0" style="bottom:1rem;">
{{{ if (isAdminOrGlobalMod || (!config.disableChatMessageEditing && messages.self)) }}}
<button class="btn btn-sm btn-link" data-action="edit" title="[[topic:edit]]"><i class="fa fa-pencil"></i></button>
{{{ end }}}
{{{ if (isAdminOrGlobalMod || isOwner )}}}
<button class="btn btn-sm btn-link" data-action="pin" title="[[modules:chat.pin-message]]"><i class="fa fa-thumbtack"></i></button>
<button class="btn btn-sm btn-link" data-action="unpin" title="[[modules:chat.unpin-message]]"><i class="fa fa-thumbtack fa-rotate-90"></i></button>
{{{ end }}}
</div>
</div>
</div>
</li>
{{{ end }}}

View File

@@ -0,0 +1,12 @@
<div component="chat/messages/pinned/container" class="d-flex flex-column expanded-chat border-start hidden" style="min-width:340px; width: 340px;">
<h3 class="fs-6 p-1 mb-0 text-center text-secondary">[[modules:chat.pinned-messages]]</h3>
<div component="chat/messages/pinned/empty" class="text-center p-4 d-flex flex-column">
<div class="p-4"><i class="fa-solid fa-wind fs-2 text-muted"></i></div>
<div class="text-xs fw-semibold text-muted">[[modules:chat.no-pinned-messages]]</div>
</div>
<ul component="chat/messages/pinned" class="chat-content list-unstyled d-flex flex-column gap-1 p-1 overflow-auto">
</ul>
</div>

View File

@@ -0,0 +1 @@
<!-- This partial intentionally left blank; overwritten by nodebb-plugin-reactions -->

View File

@@ -0,0 +1,55 @@
{{{ if (loadingMore && @first)}}}
<hr class="my-1" />
{{{ end }}}
<div component="chat/recent/room" data-roomid="{./roomId}" data-full="1" class="rounded-1 {{{ if ./unread }}}unread{{{ end }}}">
<div class="d-flex gap-1 justify-content-between">
<div class="chat-room-btn position-relative d-flex flex-grow-1 gap-2 justify-content-start align-items-start btn-ghost-sm ff-sans">
<div class="main-avatar">
{{{ if ./users.length }}}
{{{ if ./groupChat}}}
<div class="position-relative stacked-avatars">
<span class="text-decoration-none position-absolute" href="{config.relative_path}/user/{./users.1.userslug}">{buildAvatar(./users.1, "24px", true)}</span>
<span class="text-decoration-none position-absolute" href="{config.relative_path}/user/{./users.0.userslug}" >{buildAvatar(./users.0, "24px", true)}</span>
</div>
{{{ else }}}
<span href="{config.relative_path}/user/{./users.0.userslug}" class="text-decoration-none">{buildAvatar(./users.0, "32px", true)}</span>
{{{ end }}}
{{{ else }}}
<span class="avatar avatar-rounded text-bg-warning" component="avatar/icon" style="--avatar-size: 32px;">?</span>
{{{ end }}}
</div>
<div class="d-flex flex-grow-1 flex-column w-100">
<div component="chat/room/title" class="room-name fw-semibold text-xs text-break">
{{{ if ./roomName}}}
{./roomName}
{{{ else }}}
{{{ if !./lastUser.uid }}}
[[modules:chat.no-users-in-room]]
{{{ else }}}
{./usernames}
{{{ end }}}
{{{ end }}}
</div>
{{{ if ./teaser }}}
<div class="teaser-content text-sm line-clamp-3 text-break">
<span href="#" class="text-decoration-none">{buildAvatar(./teaser.user, "14px", true)}</span>
<strong class="text-xs fw-semibold teaser-username">{./teaser.user.username}:</strong>
{./teaser.content}
</div>
<div class="teaser-timestamp text-muted text-xs">{{{ if ./teaser.timeagoLong }}}{./teaser.timeagoLong}{{{ else }}}<span class="timeago" title="{./teaser.timestampISO}"></span>{{{ end }}}</div>
{{{ end }}}
</div>
</div>
<div>
<button class="mark-read btn-ghost-sm" style="width: 1.5rem; height: 1.5rem;">
<i class="unread fa fa-2xs fa-circle text-primary {{{ if !./unread }}}hidden{{{ end }}}"></i>
<i class="read fa fa-2xs fa-circle-o text-secondary {{{ if ./unread }}}hidden{{{ end }}}"></i>
</button>
</div>
</div>
</div>
{{{ if !@last }}}
<hr class="my-1" />
{{{ end }}}

View File

@@ -0,0 +1,3 @@
<div class="position-relative">
<div component="chat/messages/scroll-up-alert" class="py-1 mt-1 position-absolute start-50 top-50 translate-middle text-sm scroll-up-alert alert alert-info d-none d-md-block text-nowrap hidden" role="button" style="z-index: 500;"><i class="fa fa-fw fa-arrow-down"></i> [[modules:chat.scroll-up-alert]]</div>
</div>

View File

@@ -0,0 +1,7 @@
<li component="chat/system-message" class="system-message text-muted small py-2 gap-3 d-flex align-items-center justify-content-center" data-mid="{messages.messageId}" data-uid="{messages.fromuid}" data-index="{messages.index}" data-self="{messages.self}" data-break="0" data-timestamp="{messages.timestamp}">
<hr class="d-inline-block my-1" style="width: 10%;"/>
<div>
[[modules:chat.system.{messages.content}, {messages.fromUser.username}, {messages.timestampISO}]]
</div>
<hr class="d-inline-block my-1" style="width: 10%;"/>
</li>

View File

@@ -0,0 +1,11 @@
<div component="chat/user/list" class="border-start hidden d-flex flex-column gap-1 p-1 overflow-auto" style="min-width:240px; width: 240px;">
{{{ each users }}}
<a data-index="{./index}" data-uid="{./uid}" class="btn-ghost-sm ff-secondary d-flex justify-content-start align-items-center gap-2 {{{ if ./online }}}online{{{ end}}}" href="{config.relative_path}/uid/{./uid}">
<div>{buildAvatar(users, "24px", true)}</div>
<div class="d-flex gap-1 flex-grow-1 text-nowrap text-truncate">
<span component="chat/user/list/username" class="text-truncate">{./username}</span>
{{{ if ./isOwner }}}<span><i class="fa fa-star text-warning" data-bs-toggle="tooltip" title="[[modules:chat.owner]]"></i></span>{{{ end }}}
</div>
</a>
{{{ end }}}
</div>

View File

@@ -0,0 +1 @@
<a href="{config.relative_path}/user/{rooms.users.userslug}" class="text-decoration-none">{buildAvatar(rooms.users, "32px", true)}</a>