Added new moderator notes system.
This commit is contained in:
parent
ee304af133
commit
3299d73df2
20 changed files with 918 additions and 61 deletions
|
@ -160,21 +160,6 @@ html {
|
||||||
@include home/landingv2.css;
|
@include home/landingv2.css;
|
||||||
|
|
||||||
@include manage/_manage.css;
|
@include manage/_manage.css;
|
||||||
@include manage/blacklist.css;
|
|
||||||
@include manage/changelog-actions-tags.css;
|
|
||||||
@include manage/emote.css;
|
|
||||||
@include manage/emotes.css;
|
|
||||||
@include manage/navigation.css;
|
|
||||||
@include manage/role-item.css;
|
|
||||||
@include manage/roles.css;
|
|
||||||
@include manage/settings.css;
|
|
||||||
@include manage/statistic.css;
|
|
||||||
@include manage/statistics.css;
|
|
||||||
@include manage/tag.css;
|
|
||||||
@include manage/tags.css;
|
|
||||||
@include manage/user-item.css;
|
|
||||||
@include manage/user.css;
|
|
||||||
@include manage/users.css;
|
|
||||||
|
|
||||||
@include news/container.css;
|
@include news/container.css;
|
||||||
@include news/feeds.css;
|
@include news/feeds.css;
|
||||||
|
|
|
@ -23,3 +23,21 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@include manage/blacklist.css;
|
||||||
|
@include manage/changelog-actions-tags.css;
|
||||||
|
@include manage/emote.css;
|
||||||
|
@include manage/emotes.css;
|
||||||
|
@include manage/navigation.css;
|
||||||
|
@include manage/note.css;
|
||||||
|
@include manage/notes.css;
|
||||||
|
@include manage/role-item.css;
|
||||||
|
@include manage/roles.css;
|
||||||
|
@include manage/settings.css;
|
||||||
|
@include manage/statistic.css;
|
||||||
|
@include manage/statistics.css;
|
||||||
|
@include manage/tag.css;
|
||||||
|
@include manage/tags.css;
|
||||||
|
@include manage/user-item.css;
|
||||||
|
@include manage/user.css;
|
||||||
|
@include manage/users.css;
|
||||||
|
|
88
assets/misuzu.css/manage/note.css
Normal file
88
assets/misuzu.css/manage/note.css
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
.manage__note {
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage__note--view .manage__note--editing,
|
||||||
|
.manage__note--edit .manage__note--viewing {
|
||||||
|
display: none !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage__note__header {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage__note__title {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
font-size: 1.4em;
|
||||||
|
line-height: 1.3em;
|
||||||
|
}
|
||||||
|
.manage__note__title__text {
|
||||||
|
padding: 2px 5px;
|
||||||
|
}
|
||||||
|
.manage__note__title input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.manage__note__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
gap: 1px;
|
||||||
|
padding: 1px;
|
||||||
|
margin: 1px;
|
||||||
|
}
|
||||||
|
.manage__note__action {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage__note__attributes {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage__note__attribute {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.manage__note__created__icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage__note__author a,
|
||||||
|
.manage__note__user a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.manage__note__author__name a,
|
||||||
|
.manage__note__user__name a {
|
||||||
|
font-weight: bold;
|
||||||
|
display: inline-block;
|
||||||
|
padding-top: 2px;
|
||||||
|
border-bottom: 2px solid var(--user-colour, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage__note__body {
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage__note__nobody {
|
||||||
|
text-align: center;
|
||||||
|
font-size: .9em;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage__note__editor {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.manage__note__editor textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
122
assets/misuzu.css/manage/notes.css
Normal file
122
assets/misuzu.css/manage/notes.css
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
.manage__notes__pagination {
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage__notes__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage__notes__item {
|
||||||
|
padding: 2px;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
.manage__notes__item:not(:last-child) {
|
||||||
|
border-bottom: 1px solid var(--accent-colour);
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage__notes__item__header {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.manage__notes__item__title {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
font-size: 1.4em;
|
||||||
|
line-height: 1.3em;
|
||||||
|
padding: 2px 5px;
|
||||||
|
}
|
||||||
|
.manage__notes__item__title a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.manage__notes__item__title a:hover,
|
||||||
|
.manage__notes__item__title a:focus {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.manage__notes__item__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
gap: 1px;
|
||||||
|
padding: 1px;
|
||||||
|
margin: 1px;
|
||||||
|
}
|
||||||
|
.manage__notes__item__action {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage__notes__item__attributes {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage__notes__item__attribute {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.manage__notes__item__created__icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage__notes__item__author a,
|
||||||
|
.manage__notes__item__user a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.manage__notes__item__author__name a,
|
||||||
|
.manage__notes__item__user__name a {
|
||||||
|
font-weight: bold;
|
||||||
|
display: inline-block;
|
||||||
|
padding-top: 2px;
|
||||||
|
border-bottom: 2px solid var(--user-colour, #fff);
|
||||||
|
}
|
||||||
|
.manage__notes__item__user__filter a {
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
transition: background .2s;
|
||||||
|
}
|
||||||
|
.manage__notes__item__user__filter a:hover,
|
||||||
|
.manage__notes__item__user__filter a:focus {
|
||||||
|
background: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
.manage__notes__item__user__filter a:active {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage__notes__item__body {
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage__notes__item__nobody {
|
||||||
|
text-align: center;
|
||||||
|
font-size: .9em;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage__notes__item__continue {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.manage__notes__item__continue a {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 5px;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
transition: background .2s;
|
||||||
|
}
|
||||||
|
.manage__notes__item__continue a:hover,
|
||||||
|
.manage__notes__item__continue a:focus {
|
||||||
|
background: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
.manage__notes__item__continue a:active {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
46
database/2023_07_24_201010_add_moderator_notes_table.php
Normal file
46
database/2023_07_24_201010_add_moderator_notes_table.php
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
use Index\Data\IDbConnection;
|
||||||
|
use Index\Data\Migration\IDbMigration;
|
||||||
|
|
||||||
|
final class AddModeratorNotesTable_20230724_201010 implements IDbMigration {
|
||||||
|
public function migrate(IDbConnection $conn): void {
|
||||||
|
$conn->execute('
|
||||||
|
CREATE TABLE msz_users_modnotes (
|
||||||
|
note_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
user_id INT(10) UNSIGNED NOT NULL,
|
||||||
|
author_id INT(10) UNSIGNED NULL DEFAULT NULL,
|
||||||
|
note_created TIMESTAMP NOT NULL DEFAULT current_timestamp(),
|
||||||
|
note_title VARCHAR(255) NOT NULL,
|
||||||
|
note_body TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (note_id),
|
||||||
|
KEY users_modnotes_user_foreign (user_id),
|
||||||
|
KEY users_modnotes_author_foreign (author_id),
|
||||||
|
KEY users_modnotes_created_index (note_created),
|
||||||
|
CONSTRAINT users_modnotes_user_foreign
|
||||||
|
FOREIGN KEY (user_id)
|
||||||
|
REFERENCES msz_users (user_id)
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT users_modnotes_author_foreign
|
||||||
|
FOREIGN KEY (author_id)
|
||||||
|
REFERENCES msz_users (user_id)
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB COLLATE=utf8mb4_bin
|
||||||
|
');
|
||||||
|
|
||||||
|
// migrate existing notes
|
||||||
|
$conn->execute('
|
||||||
|
INSERT INTO msz_users_modnotes (user_id, author_id, note_created, note_title, note_body)
|
||||||
|
SELECT user_id, issuer_id, warning_created, warning_note, COALESCE(warning_note_private, "")
|
||||||
|
FROM msz_user_warnings
|
||||||
|
WHERE warning_type = 0
|
||||||
|
');
|
||||||
|
|
||||||
|
// delete notes from the warnings table
|
||||||
|
$conn->execute('DELETE FROM msz_user_warnings WHERE warning_type = 0');
|
||||||
|
|
||||||
|
// for good measure update silences to bans since i forgot to do that as a migration
|
||||||
|
$conn->execute('UPDATE msz_user_warnings SET warning_type = 3 WHERE warning_type = 2');
|
||||||
|
}
|
||||||
|
}
|
87
public-legacy/manage/users/note.php
Normal file
87
public-legacy/manage/users/note.php
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
use Misuzu\Users\User;
|
||||||
|
|
||||||
|
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_NOTES)) {
|
||||||
|
echo render_error(403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasNoteId = filter_has_var(INPUT_GET, 'n');
|
||||||
|
$hasUserId = filter_has_var(INPUT_GET, 'u');
|
||||||
|
|
||||||
|
if((!$hasNoteId && !$hasUserId) || ($hasNoteId && $hasUserId)) {
|
||||||
|
echo render_error(400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$modNotes = $msz->getModNotes();
|
||||||
|
|
||||||
|
if($hasUserId) {
|
||||||
|
$isNew = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$userInfo = User::byId((int)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT));
|
||||||
|
} catch(RuntimeException $ex) {
|
||||||
|
echo render_error(404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$authorInfo = User::getCurrent();
|
||||||
|
} elseif($hasNoteId) {
|
||||||
|
$isNew = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$noteInfo = $modNotes->getNote((string)filter_input(INPUT_GET, 'n', FILTER_SANITIZE_NUMBER_INT));
|
||||||
|
} catch(RuntimeException $ex) {
|
||||||
|
echo render_error(404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($_SERVER['REQUEST_METHOD'] === 'GET' && filter_has_var(INPUT_GET, 'delete')) {
|
||||||
|
if(CSRF::validateRequest()) {
|
||||||
|
$modNotes->deleteNote($noteInfo);
|
||||||
|
$msz->createAuditLog('MOD_NOTE_DELETE', [$noteInfo->getId(), $noteInfo->getUserId()]);
|
||||||
|
url_redirect('manage-users-notes', ['user' => $noteInfo->getUserId()]);
|
||||||
|
} else render_error(403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userInfo = User::byId((int)$noteInfo->getUserId());
|
||||||
|
$authorInfo = $noteInfo->hasAuthorId() ? User::byId((int)$noteInfo->getAuthorId()) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||||
|
$title = trim((string)filter_input(INPUT_POST, 'mn_title'));
|
||||||
|
$body = trim((string)filter_input(INPUT_POST, 'mn_body'));
|
||||||
|
|
||||||
|
if($isNew) {
|
||||||
|
$noteInfo = $modNotes->createNote($userInfo, $title, $body, $authorInfo);
|
||||||
|
} else {
|
||||||
|
if($title === $noteInfo->getTitle())
|
||||||
|
$title = null;
|
||||||
|
if($body === $noteInfo->getBody())
|
||||||
|
$body = null;
|
||||||
|
|
||||||
|
if($title !== null || $body !== null)
|
||||||
|
$modNotes->updateNote($noteInfo, $title, $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
$msz->createAuditLog(
|
||||||
|
$isNew ? 'MOD_NOTE_CREATE' : 'MOD_NOTE_UPDATE',
|
||||||
|
[$noteInfo->getId(), $userInfo->getId()]
|
||||||
|
);
|
||||||
|
|
||||||
|
// this is easier
|
||||||
|
url_redirect('manage-users-note', ['note' => $noteInfo->getId()]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Template::render('manage.users.note', [
|
||||||
|
'note_new' => $isNew,
|
||||||
|
'note_info' => $noteInfo ?? null,
|
||||||
|
'note_user' => $userInfo,
|
||||||
|
'note_author' => $authorInfo,
|
||||||
|
]);
|
63
public-legacy/manage/users/notes.php
Normal file
63
public-legacy/manage/users/notes.php
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
use Misuzu\Users\User;
|
||||||
|
|
||||||
|
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_NOTES)) {
|
||||||
|
echo render_error(403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userInfos = [
|
||||||
|
(string)User::getCurrent()->getId() => User::getCurrent(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$filterUser = null;
|
||||||
|
if(filter_has_var(INPUT_GET, 'u')) {
|
||||||
|
$filterUserId = (int)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT);
|
||||||
|
try {
|
||||||
|
$filterUser = User::byId($filterUserId);
|
||||||
|
$userInfos[(string)$filterUser->getId()] = $filterUser;
|
||||||
|
} catch(RuntimeException $ex) {
|
||||||
|
echo render_error(404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$modNotes = $msz->getModNotes();
|
||||||
|
$pagination = new Pagination($modNotes->countNotes(userInfo: $filterUser), 10);
|
||||||
|
|
||||||
|
if(!$pagination->hasValidOffset()) {
|
||||||
|
echo render_error(404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$notes = [];
|
||||||
|
$noteInfos = $modNotes->getNotes(userInfo: $filterUser, pagination: $pagination);
|
||||||
|
|
||||||
|
foreach($noteInfos as $noteInfo) {
|
||||||
|
if(array_key_exists($noteInfo->getUserId(), $userInfos))
|
||||||
|
$userInfo = $userInfos[$noteInfo->getUserId()];
|
||||||
|
else
|
||||||
|
$userInfos[$noteInfo->getUserId()] = $userInfo = User::byId((int)$noteInfo->getUserId());
|
||||||
|
|
||||||
|
if(!$noteInfo->hasAuthorId())
|
||||||
|
$authorInfo = null;
|
||||||
|
elseif(array_key_exists($noteInfo->getAuthorId(), $userInfos))
|
||||||
|
$authorInfo = $userInfos[$noteInfo->getAuthorId()];
|
||||||
|
else
|
||||||
|
$userInfos[$noteInfo->getAuthorId()] = $authorInfo = User::byId((int)$noteInfo->getAuthorId());
|
||||||
|
|
||||||
|
$notes[] = [
|
||||||
|
'info' => $noteInfo,
|
||||||
|
'user' => $userInfo,
|
||||||
|
'author' => $authorInfo,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
Template::render('manage.users.notes', [
|
||||||
|
'manage_notes' => $notes,
|
||||||
|
'manage_notes_pagination' => $pagination,
|
||||||
|
'manage_notes_filter_user' => $filterUser,
|
||||||
|
]);
|
|
@ -25,6 +25,7 @@ try {
|
||||||
|
|
||||||
$canEdit = $currentUser->hasAuthorityOver($userInfo);
|
$canEdit = $currentUser->hasAuthorityOver($userInfo);
|
||||||
$canEditPerms = $canEdit && perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ_PERM_USER_MANAGE_PERMS);
|
$canEditPerms = $canEdit && perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ_PERM_USER_MANAGE_PERMS);
|
||||||
|
$canManageNotes = perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ_PERM_USER_MANAGE_NOTES);
|
||||||
$permissions = manage_perms_list(perms_get_user_raw($userId));
|
$permissions = manage_perms_list(perms_get_user_raw($userId));
|
||||||
|
|
||||||
if(CSRF::validateRequest() && $canEdit) {
|
if(CSRF::validateRequest() && $canEdit) {
|
||||||
|
@ -187,5 +188,6 @@ Template::render('manage.users.user', [
|
||||||
'manage_roles' => UserRole::all(true),
|
'manage_roles' => UserRole::all(true),
|
||||||
'can_edit_user' => $canEdit,
|
'can_edit_user' => $canEdit,
|
||||||
'can_edit_perms' => $canEdit && $canEditPerms,
|
'can_edit_perms' => $canEdit && $canEditPerms,
|
||||||
|
'can_manage_notes' => $canManageNotes,
|
||||||
'permissions' => $permissions ?? [],
|
'permissions' => $permissions ?? [],
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -152,7 +152,6 @@ Template::render('manage.users.warnings', [
|
||||||
'user' => $warningsUserInfo,
|
'user' => $warningsUserInfo,
|
||||||
'durations' => $warningDurations,
|
'durations' => $warningDurations,
|
||||||
'types' => [
|
'types' => [
|
||||||
UserWarning::TYPE_NOTE => 'Note',
|
|
||||||
UserWarning::TYPE_WARN => 'Warning',
|
UserWarning::TYPE_WARN => 'Warning',
|
||||||
UserWarning::TYPE_BAHN => 'Ban',
|
UserWarning::TYPE_BAHN => 'Ban',
|
||||||
],
|
],
|
||||||
|
|
|
@ -122,5 +122,9 @@ class AuditLogInfo {
|
||||||
'EMOTICON_DELETE' => 'Deleted emoticon #%s.',
|
'EMOTICON_DELETE' => 'Deleted emoticon #%s.',
|
||||||
'EMOTICON_ORDER' => 'Changed order of emoticon #%s.',
|
'EMOTICON_ORDER' => 'Changed order of emoticon #%s.',
|
||||||
'EMOTICON_ALIAS' => 'Added alias "%2$s" to emoticon #%1$s.',
|
'EMOTICON_ALIAS' => 'Added alias "%2$s" to emoticon #%1$s.',
|
||||||
|
|
||||||
|
'MOD_NOTE_CREATE' => 'Added moderator note #%d to user #%d.',
|
||||||
|
'MOD_NOTE_UPDATE' => 'Edited moderator note #%d on user #%d.',
|
||||||
|
'MOD_NOTE_DELETE' => 'Removed moderator note #%d from user #%d.',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ use Misuzu\Config\IConfig;
|
||||||
use Misuzu\Emoticons\Emotes;
|
use Misuzu\Emoticons\Emotes;
|
||||||
use Misuzu\News\News;
|
use Misuzu\News\News;
|
||||||
use Misuzu\SharpChat\SharpChatRoutes;
|
use Misuzu\SharpChat\SharpChatRoutes;
|
||||||
|
use Misuzu\Users\ModNotes;
|
||||||
use Misuzu\Users\User;
|
use Misuzu\Users\User;
|
||||||
use Index\Data\IDbConnection;
|
use Index\Data\IDbConnection;
|
||||||
use Index\Data\Migration\IDbMigrationRepo;
|
use Index\Data\Migration\IDbMigrationRepo;
|
||||||
|
@ -37,6 +38,7 @@ class MisuzuContext {
|
||||||
private Comments $comments;
|
private Comments $comments;
|
||||||
private LoginAttempts $loginAttempts;
|
private LoginAttempts $loginAttempts;
|
||||||
private RecoveryTokens $recoveryTokens;
|
private RecoveryTokens $recoveryTokens;
|
||||||
|
private ModNotes $modNotes;
|
||||||
|
|
||||||
public function __construct(IDbConnection $dbConn, IConfig $config) {
|
public function __construct(IDbConnection $dbConn, IConfig $config) {
|
||||||
$this->dbConn = $dbConn;
|
$this->dbConn = $dbConn;
|
||||||
|
@ -48,6 +50,7 @@ class MisuzuContext {
|
||||||
$this->comments = new Comments($this->dbConn);
|
$this->comments = new Comments($this->dbConn);
|
||||||
$this->loginAttempts = new LoginAttempts($this->dbConn);
|
$this->loginAttempts = new LoginAttempts($this->dbConn);
|
||||||
$this->recoveryTokens = new RecoveryTokens($this->dbConn);
|
$this->recoveryTokens = new RecoveryTokens($this->dbConn);
|
||||||
|
$this->modNotes = new ModNotes($this->dbConn);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDbConn(): IDbConnection {
|
public function getDbConn(): IDbConnection {
|
||||||
|
@ -103,6 +106,10 @@ class MisuzuContext {
|
||||||
return $this->recoveryTokens;
|
return $this->recoveryTokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getModNotes(): ModNotes {
|
||||||
|
return $this->modNotes;
|
||||||
|
}
|
||||||
|
|
||||||
public function createAuditLog(string $action, array $params = [], User|string|null $userInfo = null): void {
|
public function createAuditLog(string $action, array $params = [], User|string|null $userInfo = null): void {
|
||||||
if($userInfo === null && User::hasCurrent())
|
if($userInfo === null && User::hasCurrent())
|
||||||
$userInfo = User::getCurrent();
|
$userInfo = User::getCurrent();
|
||||||
|
|
68
src/Users/ModNoteInfo.php
Normal file
68
src/Users/ModNoteInfo.php
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu\Users;
|
||||||
|
|
||||||
|
use Index\DateTime;
|
||||||
|
use Index\Data\IDbResult;
|
||||||
|
|
||||||
|
class ModNoteInfo {
|
||||||
|
private string $noteId;
|
||||||
|
private string $userId;
|
||||||
|
private ?string $authorId;
|
||||||
|
private int $created;
|
||||||
|
private string $title;
|
||||||
|
private string $body;
|
||||||
|
|
||||||
|
public function __construct(IDbResult $result) {
|
||||||
|
$this->noteId = (string)$result->getInteger(0);
|
||||||
|
$this->userId = (string)$result->getInteger(1);
|
||||||
|
$this->authorId = $result->isNull(2) ? null : (string)$result->getInteger(2);
|
||||||
|
$this->created = $result->getInteger(3);
|
||||||
|
$this->title = $result->getString(4);
|
||||||
|
$this->body = $result->getString(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): string {
|
||||||
|
return $this->noteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUserId(): string {
|
||||||
|
return $this->userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasAuthorId(): bool {
|
||||||
|
return $this->authorId !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAuthorId(): ?string {
|
||||||
|
return $this->authorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedTime(): int {
|
||||||
|
return $this->created;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): DateTime {
|
||||||
|
return DateTime::fromUnixTimeSeconds($this->created);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string {
|
||||||
|
return $this->title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasBody(): bool {
|
||||||
|
return trim($this->body) !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBody(): string {
|
||||||
|
return $this->body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFirstParagraph(): string {
|
||||||
|
$index = mb_strpos($this->body, "\n");
|
||||||
|
return $index === false ? $this->body : mb_substr($this->body, 0, $index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasMoreParagraphs(): bool {
|
||||||
|
return mb_strpos($this->body, "\n") !== false;
|
||||||
|
}
|
||||||
|
}
|
175
src/Users/ModNotes.php
Normal file
175
src/Users/ModNotes.php
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu\Users;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use RuntimeException;
|
||||||
|
use Index\Data\DbStatementCache;
|
||||||
|
use Index\Data\DbTools;
|
||||||
|
use Index\Data\IDbConnection;
|
||||||
|
use Misuzu\Pagination;
|
||||||
|
use Misuzu\Users\User;
|
||||||
|
|
||||||
|
class ModNotes {
|
||||||
|
private IDbConnection $dbConn;
|
||||||
|
private DbStatementCache $cache;
|
||||||
|
|
||||||
|
public function __construct(IDbConnection $dbConn) {
|
||||||
|
$this->dbConn = $dbConn;
|
||||||
|
$this->cache = new DbStatementCache($dbConn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countNotes(
|
||||||
|
User|string|null $userInfo = null,
|
||||||
|
User|string|null $authorInfo = null
|
||||||
|
): int {
|
||||||
|
if($userInfo instanceof User)
|
||||||
|
$userInfo = (string)$userInfo->getId();
|
||||||
|
if($authorInfo instanceof User)
|
||||||
|
$authorInfo = (string)$authorInfo->getId();
|
||||||
|
|
||||||
|
$hasUserInfo = $userInfo !== null;
|
||||||
|
$hasAuthorInfo = $authorInfo !== null;
|
||||||
|
|
||||||
|
$args = 0;
|
||||||
|
$query = 'SELECT COUNT(*) FROM msz_users_modnotes';
|
||||||
|
if($hasUserInfo) {
|
||||||
|
++$args;
|
||||||
|
$query .= ' WHERE user_id = ?';
|
||||||
|
}
|
||||||
|
if($hasAuthorInfo)
|
||||||
|
$query .= sprintf(' %s author_id = ?', ++$args > 1 ? 'AND' : 'WHERE');
|
||||||
|
|
||||||
|
$args = 0;
|
||||||
|
$stmt = $this->cache->get($query);
|
||||||
|
if($hasUserInfo)
|
||||||
|
$stmt->addParameter(++$args, $userInfo);
|
||||||
|
if($hasAuthorInfo)
|
||||||
|
$stmt->addParameter(++$args, $authorInfo);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
$result = $stmt->getResult();
|
||||||
|
$count = 0;
|
||||||
|
|
||||||
|
if($result->next())
|
||||||
|
$count = $result->getInteger(0);
|
||||||
|
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNotes(
|
||||||
|
User|string|null $userInfo = null,
|
||||||
|
User|string|null $authorInfo = null,
|
||||||
|
?Pagination $pagination = null
|
||||||
|
): array {
|
||||||
|
if($userInfo instanceof User)
|
||||||
|
$userInfo = (string)$userInfo->getId();
|
||||||
|
if($authorInfo instanceof User)
|
||||||
|
$authorInfo = (string)$authorInfo->getId();
|
||||||
|
|
||||||
|
$hasUserInfo = $userInfo !== null;
|
||||||
|
$hasAuthorInfo = $authorInfo !== null;
|
||||||
|
$hasPagination = $pagination !== null;
|
||||||
|
|
||||||
|
$args = 0;
|
||||||
|
$query = 'SELECT note_id, user_id, author_id, UNIX_TIMESTAMP(note_created), note_title, note_body FROM msz_users_modnotes';
|
||||||
|
if($hasUserInfo) {
|
||||||
|
++$args;
|
||||||
|
$query .= ' WHERE user_id = ?';
|
||||||
|
}
|
||||||
|
if($hasAuthorInfo)
|
||||||
|
$query .= sprintf(' %s author_id = ?', ++$args > 1 ? 'AND' : 'WHERE');
|
||||||
|
$query .= ' ORDER BY note_created DESC';
|
||||||
|
if($hasPagination)
|
||||||
|
$query .= ' LIMIT ? OFFSET ?';
|
||||||
|
|
||||||
|
$args = 0;
|
||||||
|
$stmt = $this->cache->get($query);
|
||||||
|
if($hasUserInfo)
|
||||||
|
$stmt->addParameter(++$args, $userInfo);
|
||||||
|
if($hasAuthorInfo)
|
||||||
|
$stmt->addParameter(++$args, $authorInfo);
|
||||||
|
if($hasPagination) {
|
||||||
|
$stmt->addParameter(++$args, $pagination->getRange());
|
||||||
|
$stmt->addParameter(++$args, $pagination->getOffset());
|
||||||
|
}
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
$result = $stmt->getResult();
|
||||||
|
$notes = [];
|
||||||
|
|
||||||
|
while($result->next())
|
||||||
|
$notes[] = new ModNoteInfo($result);
|
||||||
|
|
||||||
|
return $notes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNote(string $noteId): ModNoteInfo {
|
||||||
|
$stmt = $this->cache->get('SELECT note_id, user_id, author_id, UNIX_TIMESTAMP(note_created), note_title, note_body FROM msz_users_modnotes WHERE note_id = ?');
|
||||||
|
$stmt->addParameter(1, $noteId);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->getResult();
|
||||||
|
|
||||||
|
if(!$result->next())
|
||||||
|
throw new RuntimeException('No note with ID $noteId found.');
|
||||||
|
|
||||||
|
return new ModNoteInfo($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createNote(
|
||||||
|
User|string $userInfo,
|
||||||
|
string $title,
|
||||||
|
string $body,
|
||||||
|
User|string|null $authorInfo = null
|
||||||
|
): ModNoteInfo {
|
||||||
|
if($userInfo instanceof User)
|
||||||
|
$userInfo = (string)$userInfo->getId();
|
||||||
|
if($authorInfo instanceof User)
|
||||||
|
$authorInfo = (string)$authorInfo->getId();
|
||||||
|
|
||||||
|
$stmt = $this->cache->get('INSERT INTO msz_users_modnotes (user_id, author_id, note_title, note_body) VALUES (?, ?, ?, ?)');
|
||||||
|
$stmt->addParameter(1, $userInfo);
|
||||||
|
$stmt->addParameter(2, $authorInfo);
|
||||||
|
$stmt->addParameter(3, $title);
|
||||||
|
$stmt->addParameter(4, $body);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
return $this->getNote((string)$this->dbConn->getLastInsertId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteNote(ModNoteInfo|string|array $noteInfos): void {
|
||||||
|
if(!is_array($noteInfos))
|
||||||
|
$noteInfos = [$noteInfos];
|
||||||
|
|
||||||
|
$stmt = $this->cache->get(sprintf(
|
||||||
|
'DELETE FROM msz_users_modnotes WHERE note_id IN (%s)',
|
||||||
|
DbTools::prepareListString($noteInfos)
|
||||||
|
));
|
||||||
|
|
||||||
|
$args = 0;
|
||||||
|
foreach($noteInfos as $noteInfo) {
|
||||||
|
if($noteInfo instanceof ModNoteInfo)
|
||||||
|
$noteInfo = $noteInfo->getId();
|
||||||
|
elseif(!is_string($noteInfos))
|
||||||
|
throw new InvalidArgumentException('$noteInfos must be strings of instances of ModNoteInfo');
|
||||||
|
|
||||||
|
$stmt->addParameter(++$args, $noteInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateNote(
|
||||||
|
ModNoteInfo|string $noteInfo,
|
||||||
|
?string $title = null,
|
||||||
|
?string $body = null
|
||||||
|
): void {
|
||||||
|
if($noteInfo instanceof ModNoteInfo)
|
||||||
|
$noteInfo = $noteInfo->getId();
|
||||||
|
|
||||||
|
$stmt = $this->cache->get('UPDATE msz_users_modnotes SET note_title = COALESCE(?, note_title), note_body = COALESCE(?, note_body) WHERE note_id = ?');
|
||||||
|
$stmt->addParameter(1, $title);
|
||||||
|
$stmt->addParameter(2, $body);
|
||||||
|
$stmt->addParameter(3, $noteInfo);
|
||||||
|
$stmt->execute();
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,9 +7,6 @@ use Misuzu\DB;
|
||||||
use Misuzu\Pagination;
|
use Misuzu\Pagination;
|
||||||
|
|
||||||
class UserWarning {
|
class UserWarning {
|
||||||
// Informational notes on profile, only show up for moderators
|
|
||||||
public const TYPE_NOTE = 0;
|
|
||||||
|
|
||||||
// Warning, only shows up to moderators and the user themselves
|
// Warning, only shows up to moderators and the user themselves
|
||||||
public const TYPE_WARN = 1;
|
public const TYPE_WARN = 1;
|
||||||
|
|
||||||
|
@ -17,7 +14,7 @@ class UserWarning {
|
||||||
// User will still be able to log in and change certain details but can no longer partake in community things
|
// User will still be able to log in and change certain details but can no longer partake in community things
|
||||||
public const TYPE_BAHN = 3;
|
public const TYPE_BAHN = 3;
|
||||||
|
|
||||||
private const TYPES = [self::TYPE_NOTE, self::TYPE_WARN, self::TYPE_BAHN];
|
private const TYPES = [self::TYPE_WARN, self::TYPE_BAHN];
|
||||||
|
|
||||||
private const VISIBLE_TO_STAFF = self::TYPES;
|
private const VISIBLE_TO_STAFF = self::TYPES;
|
||||||
private const VISIBLE_TO_USER = [self::TYPE_WARN, self::TYPE_BAHN];
|
private const VISIBLE_TO_USER = [self::TYPE_WARN, self::TYPE_BAHN];
|
||||||
|
@ -127,7 +124,6 @@ class UserWarning {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getType(): int { return $this->warning_type; }
|
public function getType(): int { return $this->warning_type; }
|
||||||
public function isNote(): bool { return $this->getType() === self::TYPE_NOTE; }
|
|
||||||
public function isWarning(): bool { return $this->getType() === self::TYPE_WARN; }
|
public function isWarning(): bool { return $this->getType() === self::TYPE_WARN; }
|
||||||
public function isBan(): bool { return $this->getType() === self::TYPE_BAHN; }
|
public function isBan(): bool { return $this->getType() === self::TYPE_BAHN; }
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,8 @@ function manage_get_menu(int $userId): array {
|
||||||
$menu['Users & Roles']['Users'] = url('manage-users');
|
$menu['Users & Roles']['Users'] = url('manage-users');
|
||||||
if(perms_check_user(MSZ_PERMS_USER, $userId, MSZ_PERM_USER_MANAGE_ROLES))
|
if(perms_check_user(MSZ_PERMS_USER, $userId, MSZ_PERM_USER_MANAGE_ROLES))
|
||||||
$menu['Users & Roles']['Roles'] = url('manage-roles');
|
$menu['Users & Roles']['Roles'] = url('manage-roles');
|
||||||
|
if(perms_check_user(MSZ_PERMS_USER, $userId, MSZ_PERM_USER_MANAGE_NOTES))
|
||||||
|
$menu['Users & Roles']['Notes'] = url('manage-users-notes');
|
||||||
if(perms_check_user(MSZ_PERMS_USER, $userId, MSZ_PERM_USER_MANAGE_WARNINGS))
|
if(perms_check_user(MSZ_PERMS_USER, $userId, MSZ_PERM_USER_MANAGE_WARNINGS))
|
||||||
$menu['Users & Roles']['Warnings'] = url('manage-users-warnings');
|
$menu['Users & Roles']['Warnings'] = url('manage-users-warnings');
|
||||||
|
|
||||||
|
@ -194,9 +196,14 @@ function manage_perms_list(array $rawPerms): array {
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'section' => 'manage-warnings',
|
'section' => 'manage-warnings',
|
||||||
'title' => 'Can manage bans, warnings and notes.',
|
'title' => 'Can manage bans and warnings.',
|
||||||
'perm' => MSZ_PERM_USER_MANAGE_WARNINGS,
|
'perm' => MSZ_PERM_USER_MANAGE_WARNINGS,
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'section' => 'manage-notes',
|
||||||
|
'title' => 'Can manage user notes.',
|
||||||
|
'perm' => MSZ_PERM_USER_MANAGE_NOTES,
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|
|
@ -21,6 +21,7 @@ define('MSZ_PERM_USER_MANAGE_PERMS', 0x00400000);
|
||||||
define('MSZ_PERM_USER_MANAGE_REPORTS', 0x00800000);
|
define('MSZ_PERM_USER_MANAGE_REPORTS', 0x00800000);
|
||||||
define('MSZ_PERM_USER_MANAGE_WARNINGS', 0x01000000);
|
define('MSZ_PERM_USER_MANAGE_WARNINGS', 0x01000000);
|
||||||
//define('MSZ_PERM_USER_MANAGE_BLACKLISTS', 0x02000000); // Replaced with MSZ_PERM_GENERAL_MANAGE_BLACKLIST
|
//define('MSZ_PERM_USER_MANAGE_BLACKLISTS', 0x02000000); // Replaced with MSZ_PERM_GENERAL_MANAGE_BLACKLIST
|
||||||
|
define('MSZ_PERM_USER_MANAGE_NOTES', 0x04000000);
|
||||||
|
|
||||||
define('MSZ_PERMS_CHANGELOG', 'changelog');
|
define('MSZ_PERMS_CHANGELOG', 'changelog');
|
||||||
define('MSZ_PERM_CHANGELOG_MANAGE_CHANGES', 0x00000001);
|
define('MSZ_PERM_CHANGELOG_MANAGE_CHANGES', 0x00000001);
|
||||||
|
|
|
@ -126,6 +126,9 @@ define('MSZ_URLS', [
|
||||||
'manage-user' => ['/manage/users/user.php', ['u' => '<user>']],
|
'manage-user' => ['/manage/users/user.php', ['u' => '<user>']],
|
||||||
'manage-users-warnings' => ['/manage/users/warnings.php', ['u' => '<user>']],
|
'manage-users-warnings' => ['/manage/users/warnings.php', ['u' => '<user>']],
|
||||||
'manage-users-warning-delete' => ['/manage/users/warnings.php', ['w' => '<warning>', 'delete' => '1', 'csrf' => '{csrf}']],
|
'manage-users-warning-delete' => ['/manage/users/warnings.php', ['w' => '<warning>', 'delete' => '1', 'csrf' => '{csrf}']],
|
||||||
|
'manage-users-notes' => ['/manage/users/notes.php', ['u' => '<user>']],
|
||||||
|
'manage-users-note' => ['/manage/users/note.php', ['n' => '<note>', 'u' => '<user>']],
|
||||||
|
'manage-users-note-delete' => ['/manage/users/note.php', ['n' => '<note>', 'delete' => '1', 'csrf' => '{csrf}']],
|
||||||
|
|
||||||
'manage-roles' => ['/manage/users/roles.php'],
|
'manage-roles' => ['/manage/users/roles.php'],
|
||||||
'manage-role' => ['/manage/users/role.php', ['r' => '<role>']],
|
'manage-role' => ['/manage/users/role.php', ['r' => '<role>']],
|
||||||
|
|
73
templates/manage/users/note.twig
Normal file
73
templates/manage/users/note.twig
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
{% extends 'manage/users/master.twig' %}
|
||||||
|
{% from 'macros.twig' import pagination, container_title, avatar %}
|
||||||
|
{% from '_layout/input.twig' import input_hidden, input_csrf, input_text, input_checkbox, input_select %}
|
||||||
|
|
||||||
|
{% block manage_content %}
|
||||||
|
<div class="container">
|
||||||
|
{{ container_title('<i class="fas fa-sticky-note fa-fw"></i> ' ~ (note_new ? ('Adding note to ' ~ note_user.username) : ('Editing note #' ~ note_info.id))) }}
|
||||||
|
|
||||||
|
<form method="post" enctype="multipart/form-data" action="{{ url('manage-users-note', note_new ? {'user': note_user.id} : {'note': note_info.id}) }}" class="manage__note {{ note_new ? 'manage__note--edit' : 'manage__note--view' }}">
|
||||||
|
{{ input_csrf() }}
|
||||||
|
|
||||||
|
<div class="manage__note__header">
|
||||||
|
<div class="manage__note__title">
|
||||||
|
<div class="manage__note__title__text manage__note--viewing">{{ note_info.title|default() }}</div>
|
||||||
|
<div class="manage__note--editing">{{ input_text('mn_title', '', note_info.title|default(), 'text', '', true, null, 1) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="manage__note__actions">
|
||||||
|
<a href="javascript:;" onclick="this.closest('.manage__note').classList.remove('manage__note--view'); this.closest('.manage__note').classList.add('manage__note--edit');" title="Edit" class="input__button input__button--autosize manage__notes__item__action manage__note--viewing"><i class="fas fa-pen fa-fw"></i></a>
|
||||||
|
<button class="input__button input__button--autosize manage__notes__item__action manage__note--editing" title="{{ note_new ? 'Save Note' : 'Save Changes' }}" tabindex="3"><i class="fas fa-save fa-fw"></i></button>
|
||||||
|
{% if not note_new %}
|
||||||
|
<button class="input__button input__button--autosize input__button--destroy manage__notes__item__action manage__note--editing" type="reset" title="Discard Changes" tabindex="4"><i class="fas fa-slash fa-fw"></i></button>
|
||||||
|
<a href="javascript:;" onclick="this.closest('.manage__note').classList.remove('manage__note--edit'); this.closest('.manage__note').classList.add('manage__note--view');" title="View" class="input__button input__button--autosize manage__notes__item__action manage__note--editing" tabindex="5"><i class="fas fa-eye fa-fw"></i></a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="manage__note__attributes">
|
||||||
|
{% if note_author is not null %}
|
||||||
|
<div class="manage__note__attribute manage__notes__item__author" style="--user-colour: {{ note_author.colour }}">
|
||||||
|
<div class="manage__note__author__prefix">Created by</div>
|
||||||
|
<div class="manage__note__author__avatar">
|
||||||
|
<a href="{{ url('user-profile', {'user': note_author.id}) }}">{{ avatar(note_author.id, 20, note_author.username) }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="manage__note__author__name">
|
||||||
|
<a href="{{ url('user-profile', {'user': note_author.id}) }}">{{ note_author.username }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if not note_new %}
|
||||||
|
<div class="manage__note__attribute manage__note__created">
|
||||||
|
<div class="manage__note__created__icon"><i class="fas fa-clock"></i></div>
|
||||||
|
<div class="manage__note__created__time">
|
||||||
|
<time datetime="{{ note_info.createdTime|date('c') }}" title="{{ note_info.createdTime|date('r') }}">{{ note_info.createdTime|time_format }}</time>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="manage__note__attribute manage__note__user" style="--user-colour: {{ note_user.colour }}">
|
||||||
|
<div class="manage__note__user__prefix">Regarding</div>
|
||||||
|
<div class="manage__note__user__avatar">
|
||||||
|
<a href="{{ url('manage-user', {'user': note_user.id}) }}">{{ avatar(note_user.id, 20, note_user.username) }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="manage__note__user__name">
|
||||||
|
<a href="{{ url('manage-user', {'user': note_user.id}) }}">{{ note_user.username }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if not note_new and note_info.hasBody %}
|
||||||
|
<div class="manage__note__body markdown manage__note--viewing">
|
||||||
|
{{ note_info.body|parse_text(2)|raw }}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="manage__note__nobody manage__note--viewing">
|
||||||
|
This note has no additional content.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="manage__note__editor manage__note--editing">
|
||||||
|
<textarea name="mn_body" class="input__textarea" tabindex="2">{{ note_info.body|default() }}</textarea>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
99
templates/manage/users/notes.twig
Normal file
99
templates/manage/users/notes.twig
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
{% extends 'manage/users/master.twig' %}
|
||||||
|
{% from 'macros.twig' import pagination, container_title, avatar %}
|
||||||
|
|
||||||
|
{% set notes_pagination = pagination(manage_notes_pagination, url('manage-users-notes', {'user': manage_notes_filter_user.id|default(0)})) %}
|
||||||
|
{% set notes_filtering = manage_notes_filter_user is not null %}
|
||||||
|
|
||||||
|
{% block manage_content %}
|
||||||
|
<div class="container manage__notes">
|
||||||
|
{{ container_title('<i class="fas fa-sticky-note fa-fw"></i> User Notes') }}
|
||||||
|
|
||||||
|
<div class="manage__description">
|
||||||
|
Private moderator notes, can be used for anything you'd like to share internally.
|
||||||
|
{% if not notes_filtering %}Filter by a user to create a new note.{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if notes_pagination|trim|length > 0 %}
|
||||||
|
<div class="manage__notes__pagination">
|
||||||
|
{{ notes_pagination }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if notes_filtering %}
|
||||||
|
<div class="manage__notes__actions">
|
||||||
|
<a href="{{ url('manage-users-note', {'user': manage_notes_filter_user.id}) }}" class="input__button">New Note</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="manage__notes__list">
|
||||||
|
{% for note in manage_notes %}
|
||||||
|
<div class="manage__notes__item">
|
||||||
|
<div class="manage__notes__item__header">
|
||||||
|
<div class="manage__notes__item__title"><a href="{{ url('manage-users-note', {'note': note.info.id}) }}">{{ note.info.title }}</a></div>
|
||||||
|
<div class="manage__notes__item__actions">
|
||||||
|
<a href="{{ url('manage-users-note', {'note': note.info.id}) }}" title="View/Edit" class="input__button input__button--autosize manage__notes__item__action"><i class="fas fa-pen fa-fw"></i></a>
|
||||||
|
<a href="{{ url('manage-users-note-delete', {'note': note.info.id}) }}" title="Delete" class="input__button input__button--autosize input__button--destroy manage__notes__item__action" onclick="return confirm('Are you sure?');"><i class="fas fa-times fa-fw"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="manage__notes__item__attributes">
|
||||||
|
{% if note.author is not null %}
|
||||||
|
<div class="manage__notes__item__attribute manage__notes__item__author" style="--user-colour: {{ note.author.colour }}">
|
||||||
|
<div class="manage__notes__item__author__prefix">Created by</div>
|
||||||
|
<div class="manage__notes__item__author__avatar">
|
||||||
|
<a href="{{ url('user-profile', {'user': note.author.id}) }}">{{ avatar(note.author.id, 20, note.author.username) }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="manage__notes__item__author__name">
|
||||||
|
<a href="{{ url('user-profile', {'user': note.author.id}) }}">{{ note.author.username }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="manage__notes__item__attribute manage__notes__item__created">
|
||||||
|
<div class="manage__notes__item__created__icon"><i class="fas fa-clock"></i></div>
|
||||||
|
<div class="manage__notes__item__created__time">
|
||||||
|
<time datetime="{{ note.info.createdTime|date('c') }}" title="{{ note.info.createdTime|date('r') }}">{{ note.info.createdTime|time_format }}</time>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="manage__notes__item__attribute manage__notes__item__user" style="--user-colour: {{ note.user.colour }}">
|
||||||
|
<div class="manage__notes__item__user__prefix">Regarding</div>
|
||||||
|
<div class="manage__notes__item__user__avatar">
|
||||||
|
<a href="{{ url('manage-user', {'user': note.user.id}) }}">{{ avatar(note.user.id, 20, note.user.username) }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="manage__notes__item__user__name">
|
||||||
|
<a href="{{ url('manage-user', {'user': note.user.id}) }}">{{ note.user.username }}</a>
|
||||||
|
</div>
|
||||||
|
{% if not notes_filtering %}
|
||||||
|
<div class="manage__notes__item__user__filter">
|
||||||
|
<a href="{{ url('manage-users-notes', {'user': note.user.id}) }}">Filter</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if note.info.hasBody %}
|
||||||
|
<div class="manage__notes__item__body markdown">
|
||||||
|
{% if notes_filtering %}
|
||||||
|
{{ note.info.body|parse_text(2)|raw }}
|
||||||
|
{% else %}
|
||||||
|
{{ note.info.firstParagraph|parse_text(2)|raw }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="manage__notes__item__nobody">
|
||||||
|
This note has no additional content.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if not notes_filtering and note.info.hasMoreParagraphs %}
|
||||||
|
<div class="manage__notes__item__continue">
|
||||||
|
<a href="{{ url('manage-users-note', {'note': note.info.id}) }}">Continue reading</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if notes_pagination|trim|length > 0 %}
|
||||||
|
<div class="manage__notes__pagination">
|
||||||
|
{{ notes_pagination }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -132,6 +132,20 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{% if can_manage_notes %}
|
||||||
|
<div class="container manage__user__container">
|
||||||
|
{{ container_title('Manage notes') }}
|
||||||
|
|
||||||
|
<div class="container__content">
|
||||||
|
<p>Can you tell I'm just tacking this on?</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="manage__user__buttons">
|
||||||
|
<a href="{{ url('manage-users-notes', {'user': user_info.id}) }}" class="input__button manage__user__button">View/Edit Notes</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if current_user.super %}
|
{% if current_user.super %}
|
||||||
<form method="post" action="{{ url('manage-user', {'user': user_info.id}) }}" class="container manage__user__container">
|
<form method="post" action="{{ url('manage-user', {'user': user_info.id}) }}" class="container manage__user__container">
|
||||||
{{ container_title('Send test e-mail to ' ~ user_info.username ~ ' (' ~ user_info.id ~ ')') }}
|
{{ container_title('Send test e-mail to ' ~ user_info.username ~ ' (' ~ user_info.id ~ ')') }}
|
||||||
|
|
Loading…
Reference in a new issue