move btn-ghost to core

remove "btn-outline", it can be replaced with "btn-ghost border"
move chats templates and css from harmony to core, persona nd peace will use same templates
This commit is contained in:
Barış Soner Uşaklı
2024-01-05 17:27:18 -05:00
parent 87c3db3650
commit 80d8cfda1c
22 changed files with 668 additions and 13 deletions

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%);
$btn-ghost-active-color: lighten($btn-ghost-hover-color, 5%);
$btn-ghost-hover-color-dark: mix($dark, $light, 90%);
$btn-ghost-active-color-dark: lighten($btn-ghost-hover-color-dark, 5%);
: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";

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>