diff --git a/files/assets/css/TheMotte.css b/files/assets/css/TheMotte.css
index 8e2000a55..a24bcf8c2 100644
--- a/files/assets/css/TheMotte.css
+++ b/files/assets/css/TheMotte.css
@@ -83,3 +83,157 @@ blockquote {
#frontpage .post-title a:visited, .visited {
color: #7a7a7a !important;
}
+
+.usernote-link {
+ color: var(--primary);
+ text-decoration: none;
+ background-color: transparent;
+ cursor: pointer;
+}
+
+.usernote-link:hover {
+ text-decoration: underline;
+}
+
+.modal__overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0,0,0,0.6);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index:1033 !important;
+}
+
+.modal__container {
+ background-color: #fff;
+ padding: 30px;
+ max-width: 500px;
+ max-height: 100vh;
+ border-radius: 4px;
+ overflow-y: auto;
+ box-sizing: border-box;
+}
+
+.modal__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.modal__title {
+ margin-top: 0;
+ margin-bottom: 0;
+ font-weight: 600;
+ font-size: 1.25rem;
+ line-height: 1.25;
+ color: #00449e;
+ box-sizing: border-box;
+}
+
+.modal__close {
+ background: transparent;
+ border: 0;
+}
+
+.modal__header .modal__close:before { content: "\2715"; }
+
+.modal__content {
+ margin-top: 2rem;
+ margin-bottom: 2rem;
+ line-height: 1.5;
+ color: rgba(0,0,0,.8);
+}
+
+.modal__content > .content__wrapper {
+ display: flex;
+ flex-direction: column;
+ min-width: 400px;
+}
+
+.modal__content > .content__wrapper > .table_content,
+.modal__content > .content__wrapper > .table_content > .table_note,
+.modal__content > .content__wrapper > .table_headers {
+ display: flex;
+}
+.modal__content > .content__wrapper > .table_content > .table_note {
+ margin-top: 10px;
+}
+.modal__content > .content__wrapper > .table_headers > *,
+.modal__content > .content__wrapper > .table_content > .table_note > * {
+ flex-basis: 20%;
+}
+.modal__content > .content__wrapper > .table_headers > *:nth-child(2),
+.modal__content > .content__wrapper > .table_content > .table_note > *:nth-child(2) {
+ flex-basis: 35%;
+ flex-grow: 1;
+}
+.modal__content > .content__wrapper > .table_content .table_note_message {
+ padding: 5px;
+ box-sizing: border-box;
+}
+.modal__content > .content__wrapper > .table_content > .table_note .table_note_date,
+.modal__content > .content__wrapper > .table_content > .table_note > .table_note_delete > span {
+ color: var(--primary);
+ border: none !important;
+ text-decoration: none;
+ background-color: transparent;
+ cursor: pointer;
+}
+.modal__content > .content__wrapper > .table_content > .table_note .table_note_date:hover,
+.modal__content > .content__wrapper > .table_content > .table_note > .table_note_delete > span:hover {
+ text-decoration: underline;
+}
+
+.modal__content > form {
+ display: flex;
+ flex-direction: column;
+}
+
+.modal__content > .content__wrapper > .table_content {
+ flex-direction: column;
+}
+
+.modal__content textarea {
+ resize: vertical !important;
+ min-height: 50px;
+ margin-top:20px;
+}
+
+.modal__content select {
+ display: table-cell;
+ padding: 5px;
+ box-sizing: border-box;
+ margin-top:5px;
+ vertical-align: middle;
+}
+
+.modal__btn {
+ font-size: .875rem;
+ padding-left: 1rem;
+ padding-right: 1rem;
+ padding-top: .5rem;
+ padding-bottom: .5rem;
+ background-color: #e6e6e6;
+ color: rgba(0,0,0,.8);
+ border-radius: .25rem;
+ border-style: none;
+ border-width: 0;
+ cursor: pointer;
+ -webkit-appearance: button;
+ text-transform: none;
+ overflow: visible;
+ line-height: 1.15;
+ margin: 0;
+}
+
+.micromodal-slide {
+ display: none;
+}
+
+.micromodal-slide.is-open {
+ display: block;
+}
diff --git a/files/assets/js/comments+submission_listing.js b/files/assets/js/comments+submission_listing.js
index 2a2a6e5c1..19da955a7 100644
--- a/files/assets/js/comments+submission_listing.js
+++ b/files/assets/js/comments+submission_listing.js
@@ -31,6 +31,106 @@ function popclick(author) {
; }, 1);
}
+function fillnote(user,post,comment) {
+
+ let dialog = document.getElementById("modal-1");
+ let table = "";
+
+ for(let i = 0; i < user.notes.length; ++i){
+ let note_id = "note_" + i;
+ let note = user.notes[i];
+ let date = new Date(parseInt(note.created) * 1000);
+ let date_str = date.toLocaleDateString();
+ let time_str = date.toLocaleTimeString();
+
+ let tag = "None";
+ switch(note.tag){
+ case 0: tag = "Quality"; break;
+ case 1: tag = "Good" ; break;
+ case 2: tag = "Comment"; break;
+ case 3: tag = "Warning"; break;
+ case 4: tag = "Tempban"; break;
+ case 5: tag = "Permban"; break;
+ case 6: tag = "Spam" ; break;
+ case 7: tag = "Bot" ; break;
+ }
+
+ table += "" +
+ "
" +
+ "
" +
+ "
" + note.note + "
" +
+ "
" + tag + "
" +
+ "
Delete
" +
+ "
\n"
+ }
+
+ dialog.getElementsByClassName('notes_target')[0].innerText = user.username;
+ dialog.getElementsByClassName('table_content')[0].innerHTML = table;
+
+ dialog.dataset.context = JSON.stringify({
+ 'url': user.url,
+ 'post': post,
+ 'comment': comment,
+ 'user': user.id,
+ });
+}
+
+function delete_note(element,url) {
+ let note = document.getElementById(element);
+ let id = note.dataset.id;
+
+ const xhr = new XMLHttpRequest();
+ xhr.open("POST", url + "/delete_note/" + id);
+ xhr.setRequestHeader('xhr', 'xhr');
+ xhr.responseType = 'json';
+
+ xhr.onload = function() {
+ if(xhr.status === 200) {
+ console.log(xhr.response);
+ location.reload();
+ }
+ }
+
+ var form = new FormData()
+ form.append("formkey", formkey());
+ xhr.send(form);
+}
+
+function send_note() {
+ let dialog = document.getElementById("modal-1");
+ let context = JSON.parse(dialog.dataset.context);
+
+ let note = document.querySelector("#modal-1 textarea").value;
+ let tag = document.querySelector("#modal-1 #usernote_tag").value;
+
+ const xhr = new XMLHttpRequest();
+ xhr.open("POST", context.url + "/create_note");
+ xhr.setRequestHeader('xhr', 'xhr');
+ xhr.responseType = 'json';
+ var form = new FormData()
+
+ form.append("formkey", formkey());
+ form.append("data", JSON.stringify({
+ 'note': note,
+ 'post': context.post,
+ 'comment': context.comment,
+ 'user': context.user,
+ 'tag': tag
+ }));
+
+ xhr.onload = function() {
+ if(xhr.status === 200) {
+ console.log(xhr.response);
+ location.reload();
+ }
+ }
+
+ xhr.send(form);
+}
+
document.addEventListener("click", function(){
active = document.activeElement.getAttributeNode("class");
if (active && active.nodeValue == "user-name text-decoration-none"){
diff --git a/files/assets/js/micromodal.js b/files/assets/js/micromodal.js
new file mode 100644
index 000000000..09167d26a
--- /dev/null
+++ b/files/assets/js/micromodal.js
@@ -0,0 +1 @@
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).MicroModal=t()}(this,(function(){"use strict";function e(e,t){for(var o=0;oe.length)&&(t=e.length);for(var o=0,n=new Array(t);o0&&this.registerTriggers.apply(this,t(a)),this.onClick=this.onClick.bind(this),this.onKeydown=this.onKeydown.bind(this)}var i,a,r;return i=o,(a=[{key:"registerTriggers",value:function(){for(var e=this,t=arguments.length,o=new Array(t),n=0;n0&&void 0!==arguments[0]?arguments[0]:null;if(this.activeElement=document.activeElement,this.modal.setAttribute("aria-hidden","false"),this.modal.classList.add(this.config.openClass),this.scrollBehaviour("disable"),this.addEventListeners(),this.config.awaitOpenAnimation){var o=function t(){e.modal.removeEventListener("animationend",t,!1),e.setFocusToFirstNode()};this.modal.addEventListener("animationend",o,!1)}else this.setFocusToFirstNode();this.config.onShow(this.modal,this.activeElement,t)}},{key:"closeModal",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,t=this.modal;if(this.modal.setAttribute("aria-hidden","true"),this.removeEventListeners(),this.scrollBehaviour("enable"),this.activeElement&&this.activeElement.focus&&this.activeElement.focus(),this.config.onClose(this.modal,this.activeElement,e),this.config.awaitCloseAnimation){var o=this.config.openClass;this.modal.addEventListener("animationend",(function e(){t.classList.remove(o),t.removeEventListener("animationend",e,!1)}),!1)}else t.classList.remove(this.config.openClass)}},{key:"closeModalById",value:function(e){this.modal=document.getElementById(e),this.modal&&this.closeModal()}},{key:"scrollBehaviour",value:function(e){if(this.config.disableScroll){var t=document.querySelector("body");switch(e){case"enable":Object.assign(t.style,{overflow:""});break;case"disable":Object.assign(t.style,{overflow:"hidden"})}}}},{key:"addEventListeners",value:function(){this.modal.addEventListener("touchstart",this.onClick),this.modal.addEventListener("click",this.onClick),document.addEventListener("keydown",this.onKeydown)}},{key:"removeEventListeners",value:function(){this.modal.removeEventListener("touchstart",this.onClick),this.modal.removeEventListener("click",this.onClick),document.removeEventListener("keydown",this.onKeydown)}},{key:"onClick",value:function(e){(e.target.hasAttribute(this.config.closeTrigger)||e.target.parentNode.hasAttribute(this.config.closeTrigger))&&(e.preventDefault(),e.stopPropagation(),this.closeModal(e))}},{key:"onKeydown",value:function(e){27===e.keyCode&&this.closeModal(e),9===e.keyCode&&this.retainFocus(e)}},{key:"getFocusableNodes",value:function(){var e=this.modal.querySelectorAll(n);return Array.apply(void 0,t(e))}},{key:"setFocusToFirstNode",value:function(){var e=this;if(!this.config.disableFocus){var t=this.getFocusableNodes();if(0!==t.length){var o=t.filter((function(t){return!t.hasAttribute(e.config.closeTrigger)}));o.length>0&&o[0].focus(),0===o.length&&t[0].focus()}}}},{key:"retainFocus",value:function(e){var t=this.getFocusableNodes();if(0!==t.length)if(t=t.filter((function(e){return null!==e.offsetParent})),this.modal.contains(document.activeElement)){var o=t.indexOf(document.activeElement);e.shiftKey&&0===o&&(t[t.length-1].focus(),e.preventDefault()),!e.shiftKey&&t.length>0&&o===t.length-1&&(t[0].focus(),e.preventDefault())}else t[0].focus()}}])&&e(i.prototype,a),r&&e(i,r),o}(),a=null,r=function(e){if(!document.getElementById(e))return console.warn("MicroModal: ❗Seems like you have missed %c'".concat(e,"'"),"background-color: #f8f9fa;color: #50596c;font-weight: bold;","ID somewhere in your code. Refer example below to resolve it."),console.warn("%cExample:","background-color: #f8f9fa;color: #50596c;font-weight: bold;",'')),!1},s=function(e,t){if(function(e){e.length<=0&&(console.warn("MicroModal: ❗Please specify at least one %c'micromodal-trigger'","background-color: #f8f9fa;color: #50596c;font-weight: bold;","data attribute."),console.warn("%cExample:","background-color: #f8f9fa;color: #50596c;font-weight: bold;",''))}(e),!t)return!0;for(var o in t)r(o);return!0},{init:function(e){var o=Object.assign({},{openTrigger:"data-micromodal-trigger"},e),n=t(document.querySelectorAll("[".concat(o.openTrigger,"]"))),r=function(e,t){var o=[];return e.forEach((function(e){var n=e.attributes[t].value;void 0===o[n]&&(o[n]=[]),o[n].push(e)})),o}(n,o.openTrigger);if(!0!==o.debugMode||!1!==s(n,r))for(var l in r){var c=r[l];o.targetModal=l,o.triggers=t(c),a=new i(o)}},show:function(e,t){var o=t||{};o.targetModal=e,!0===o.debugMode&&!1===r(e)||(a&&a.removeEventListeners(),(a=new i(o)).showModal())},close:function(e){e?a.closeModalById(e):a.closeModal()}});return"undefined"!=typeof window&&(window.MicroModal=l),l}));
diff --git a/files/classes/__init__.py b/files/classes/__init__.py
index 8ce4237ba..37ac578fd 100644
--- a/files/classes/__init__.py
+++ b/files/classes/__init__.py
@@ -6,6 +6,7 @@ from .domains import *
from .flags import *
from .user import *
from .userblock import *
+from .usernotes import *
from .submission import *
from .votes import *
from .domains import *
diff --git a/files/classes/comment.py b/files/classes/comment.py
index 900c02032..b1f7f93b0 100644
--- a/files/classes/comment.py
+++ b/files/classes/comment.py
@@ -57,6 +57,7 @@ class Comment(Base):
child_comments = relationship("Comment", lazy="dynamic", remote_side=[parent_comment_id], viewonly=True)
awards = relationship("AwardRelationship", viewonly=True)
reports = relationship("CommentFlag", viewonly=True)
+ notes = relationship("UserNote", back_populates="comment")
def __init__(self, *args, **kwargs):
if "created_utc" not in kwargs:
diff --git a/files/classes/submission.py b/files/classes/submission.py
index adcae93e6..c46aa6683 100644
--- a/files/classes/submission.py
+++ b/files/classes/submission.py
@@ -59,6 +59,7 @@ class Submission(Base):
reports = relationship("Flag", viewonly=True)
comments = relationship("Comment", primaryjoin="Comment.parent_submission==Submission.id")
subr = relationship("Sub", primaryjoin="foreign(Submission.sub)==remote(Sub.name)", viewonly=True)
+ notes = relationship("UserNote", back_populates="post")
bump_utc = deferred(Column(Integer, server_default=FetchedValue()))
diff --git a/files/classes/user.py b/files/classes/user.py
index d177cbbd8..607f37693 100644
--- a/files/classes/user.py
+++ b/files/classes/user.py
@@ -130,6 +130,7 @@ class User(Base):
authorizations = relationship("ClientAuth", viewonly=True)
awards = relationship("AwardRelationship", primaryjoin="User.id==AwardRelationship.user_id", viewonly=True)
referrals = relationship("User", viewonly=True)
+ notes = relationship("UserNote", foreign_keys='UserNote.reference_user', back_populates="user")
def __init__(self, **kwargs):
@@ -509,6 +510,7 @@ class User(Base):
'post_count': 0 if self.shadowbanned and not (v and (v.shadowbanned or v.admin_level > 2)) else self.post_count,
'comment_count': 0 if self.shadowbanned and not (v and (v.shadowbanned or v.admin_level > 2)) else self.comment_count,
'badges': [x.path for x in self.badges],
+ 'notes': [x.json() for x in self.notes]
}
return data
diff --git a/files/classes/usernotes.py b/files/classes/usernotes.py
new file mode 100644
index 000000000..f66bfe65a
--- /dev/null
+++ b/files/classes/usernotes.py
@@ -0,0 +1,66 @@
+import time
+from flask import *
+from sqlalchemy import *
+from sqlalchemy.orm import relationship
+from files.__main__ import Base
+from files.helpers.const import *
+from enum import Enum
+from sqlalchemy import Enum as EnumType
+
+class UserTag(Enum):
+ Quality = 0
+ Good = 1
+ Comment = 2
+ Warning = 3
+ Tempban = 4
+ Permban = 5
+ Spam = 6
+ Bot = 7
+
+class UserNote(Base):
+
+ __tablename__ = "usernotes"
+
+ id = Column(Integer, primary_key=True)
+ author_id = Column(Integer, ForeignKey("users.id"), nullable=False)
+ created_utc = Column(Integer, nullable=False)
+ reference_user = Column(Integer, ForeignKey("users.id", ondelete='CASCADE'), nullable=False)
+ reference_comment = Column(Integer, ForeignKey("comments.id", ondelete='SET NULL'))
+ reference_post = Column(Integer, ForeignKey("submissions.id", ondelete='SET NULL'))
+ note = Column(String, nullable=False)
+ tag = Column(EnumType(UserTag), nullable=False)
+
+ author = relationship("User", foreign_keys='UserNote.author_id')
+ user = relationship("User", foreign_keys='UserNote.reference_user', back_populates="notes")
+
+ comment = relationship("Comment", back_populates="notes")
+ post = relationship("Submission", back_populates="notes")
+
+ def __init__(self, *args, **kwargs):
+ if "created_utc" not in kwargs:
+ kwargs["created_utc"] = int(time.time())
+ super().__init__(*args, **kwargs)
+
+ def __repr__(self):
+ return f""
+
+ def json(self):
+ reference = None
+
+ if self.comment:
+ reference = self.comment.permalink
+ elif self.post:
+ reference = self.post.permalink
+
+ data = {'id': self.id,
+ 'author_name': self.author.username,
+ 'author_id': self.author.id,
+ 'user_name': self.user.username,
+ 'user_id': self.user.id,
+ 'created': self.created_utc,
+ 'reference': reference,
+ 'note': self.note,
+ 'tag': self.tag.value
+ }
+
+ return data
\ No newline at end of file
diff --git a/files/routes/admin.py b/files/routes/admin.py
index e8adb197a..f510b1ec2 100644
--- a/files/routes/admin.py
+++ b/files/routes/admin.py
@@ -217,6 +217,56 @@ def distribute(v, comment):
g.db.commit()
return {"message": f"Each winner has received {coinsperperson} coins!"}
+@app.post("/@/delete_note/")
+@admin_level_required(3)
+def delete_note(v,username,id):
+ g.db.query(UserNote).filter_by(id=id).delete()
+ g.db.commit()
+
+ return make_response(jsonify({
+ 'success':True, 'message': 'Note deleted', 'note': id
+ }), 200)
+
+@app.post("/@/create_note")
+@admin_level_required(3)
+def create_note(v,username):
+
+ def result(msg,succ,note):
+ return make_response(jsonify({
+ 'success':succ, 'message': msg, 'note': note
+ }), 200)
+
+ data = json.loads(request.values.get('data'))
+ user = g.db.query(User).filter_by(username=username).one_or_none()
+
+ if not user:
+ return result('User not found',False,None)
+
+ author_id = v.id
+ reference_user = user.id
+ reference_comment = data.get('comment',None)
+ reference_post = data.get('post',None)
+ note = data['note']
+ tag = UserTag(int(data['tag']))
+
+ if reference_comment:
+ reference_post = None
+ elif reference_post:
+ reference_comment = None
+
+ note = UserNote(
+ author_id=author_id,
+ reference_user=reference_user,
+ reference_comment=reference_comment,
+ reference_post=reference_post,
+ note=note,
+ tag=tag)
+
+ g.db.add(note)
+ g.db.commit()
+
+ return result('Note saved',True,note.json())
+
@app.post("/@/revert_actions")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@admin_level_required(3)
diff --git a/files/routes/users.py b/files/routes/users.py
index 1c65e728e..2bd42a3f1 100644
--- a/files/routes/users.py
+++ b/files/routes/users.py
@@ -71,16 +71,6 @@ def leaderboard_thread():
gevent.spawn(leaderboard_thread())
-
-
-
-
-
-
-
-
-
-
@app.get("/@/upvoters//posts")
@auth_required
def upvoters_posts(v, username, uid):
diff --git a/files/templates/comments.html b/files/templates/comments.html
index 23d71d028..6c1cc36a1 100644
--- a/files/templates/comments.html
+++ b/files/templates/comments.html
@@ -44,6 +44,8 @@
{% endif %}
+{% include 'usernote.html' %}
+
{% macro single_comment(c, level=1) %}
{% set ups=c.upvotes %}
@@ -201,6 +203,12 @@
{{c.print()}}
{% endif %}
{{c.author_name}}
+ {% if v and v.admin_level > 2 %}
+ _U_
+ {% endif %}
{% if c.author.customtitle %} {{c.author.customtitle | safe}}{% endif %}
{% endif %}
diff --git a/files/templates/default.html b/files/templates/default.html
index 2306f63d0..19f5476cf 100644
--- a/files/templates/default.html
+++ b/files/templates/default.html
@@ -5,6 +5,7 @@
+
{% if v %}
@@ -327,6 +328,10 @@
+
+