Added blocking service.
CI / lint (push) Failing after 26s
CI / test (3.9) (push) Failing after 25s
CI / test (3.12) (push) Failing after 26s
CI / build (push) Failing after 27s
CI / test (3.8) (push) Failing after 24s
CI / test (3.11) (push) Failing after 47s
CI / test (3.10) (push) Failing after 49s

This commit is contained in:
2025-12-10 15:39:19 +01:00
parent 2078c1115a
commit 3570ba5b99
12 changed files with 574 additions and 25 deletions
+111
View File
@@ -0,0 +1,111 @@
import { BaseComponent } from './base-component.js';
class BlockedUsersManager extends BaseComponent {
init() {
this.blockChangeHandler = () => this.render();
this.render();
this.bindEvents();
}
render() {
const blockedUsers = this.getBlock()?.getBlockedUsers() || [];
this.addClass('blocked-users-manager');
this.setHtml(`
<div class="block-header">
<h3>Blocked Users</h3>
<span class="block-count">${blockedUsers.length} blocked</span>
</div>
<form class="block-add-form">
<input type="text"
class="block-input"
placeholder="Enter username to block"
autocomplete="off"
autocapitalize="none">
<button type="submit" class="btn btn-danger block-add-btn">Block</button>
</form>
<div class="blocked-list">
${blockedUsers.length > 0 ? blockedUsers.map(username => `
<div class="blocked-item" data-username="${username}">
<span class="blocked-username">@${username}</span>
<button class="btn btn-small btn-secondary unblock-btn" data-username="${username}">
Unblock
</button>
</div>
`).join('') : `
<div class="blocked-empty">
<p>No blocked users</p>
</div>
`}
</div>
${blockedUsers.length > 0 ? `
<div class="block-actions">
<button class="btn btn-secondary btn-small clear-all-btn">Unblock All</button>
</div>
` : ''}
`);
}
bindEvents() {
this.on(this, 'submit', this.handleSubmit);
this.on(this, 'click', this.handleClick);
window.addEventListener('rantii:block-change', this.blockChangeHandler);
}
onDisconnected() {
window.removeEventListener('rantii:block-change', this.blockChangeHandler);
}
handleSubmit(e) {
e.preventDefault();
const input = this.$('.block-input');
if (!input) return;
const username = input.value.trim().replace(/^@/, '');
if (!username) return;
const block = this.getBlock();
if (block?.isBlocked(username)) {
window.app?.toast?.error(`@${username} is already blocked`);
return;
}
if (block?.block(username)) {
window.app?.toast?.success(`Blocked @${username}`);
input.value = '';
}
}
handleClick(e) {
const unblockBtn = e.target.closest('.unblock-btn');
const clearAllBtn = e.target.closest('.clear-all-btn');
if (unblockBtn) {
e.preventDefault();
e.stopPropagation();
const username = unblockBtn.dataset.username;
if (username && this.getBlock()?.unblock(username)) {
window.app?.toast?.success(`Unblocked @${username}`);
}
return;
}
if (clearAllBtn) {
e.preventDefault();
e.stopPropagation();
if (confirm('Unblock all users?')) {
this.getBlock()?.clear();
window.app?.toast?.success('All users unblocked');
}
}
}
getBlock() {
return window.app?.block;
}
}
customElements.define('blocked-users-manager', BlockedUsersManager);
export { BlockedUsersManager };
+22 -11
View File
@@ -13,10 +13,21 @@ class NotificationList extends BaseComponent {
this.notifications = [];
this.isLoading = false;
this.unreadCount = 0;
this.blockChangeHandler = () => this.render();
this.render();
this.bindEvents();
}
getBlock() {
return window.app?.block;
}
getFilteredNotifications() {
const block = this.getBlock();
if (!block) return this.notifications;
return block.filterNotifications(this.notifications);
}
async load() {
if (!this.isLoggedIn()) {
this.render();
@@ -62,7 +73,9 @@ class NotificationList extends BaseComponent {
return;
}
if (this.notifications.length === 0) {
const filteredNotifications = this.getFilteredNotifications();
if (filteredNotifications.length === 0) {
this.setHtml(`
<div class="notification-empty">
<svg viewBox="0 0 24 24" width="48" height="48">
@@ -82,7 +95,7 @@ class NotificationList extends BaseComponent {
` : ''}
</header>
<div class="notification-items">
${this.notifications.map(notif => `
${filteredNotifications.map(notif => `
<notification-item
data-notif='${JSON.stringify(notif).replace(/'/g, '&#39;')}'>
</notification-item>
@@ -90,24 +103,21 @@ class NotificationList extends BaseComponent {
</div>
`);
this.initNotificationItems();
this.initNotificationItems(filteredNotifications);
}
initNotificationItems() {
initNotificationItems(filteredNotifications) {
const items = this.$$('notification-item');
items.forEach(item => {
const notifData = item.dataset.notif;
if (notifData) {
try {
const notif = JSON.parse(notifData.replace(/&#39;/g, "'"));
item.setNotification(notif);
} catch (e) {}
items.forEach((item, index) => {
if (filteredNotifications[index]) {
item.setNotification(filteredNotifications[index]);
}
});
}
bindEvents() {
this.on(this, 'click', this.handleClick);
window.addEventListener('rantii:block-change', this.blockChangeHandler);
}
handleClick(e) {
@@ -139,6 +149,7 @@ class NotificationList extends BaseComponent {
onDisconnected() {
this.isLoading = false;
window.removeEventListener('rantii:block-change', this.blockChangeHandler);
}
getUnreadCount() {
+63 -8
View File
@@ -18,10 +18,27 @@ class RantDetail extends BaseComponent {
this.rantData = null;
this.comments = [];
this.isLoading = false;
this.blockChangeHandler = () => this.render();
this.render();
this.bindEvents();
}
getBlock() {
return window.app?.block;
}
getFilteredComments() {
const block = this.getBlock();
if (!block) return this.comments;
return block.filterComments(this.comments);
}
isRantBlocked() {
const block = this.getBlock();
if (!block || !this.rantData) return false;
return block.isBlocked(this.rantData.user_username);
}
async load(rantId) {
this.setAttr('rant-id', rantId);
this.isLoading = true;
@@ -69,6 +86,30 @@ class RantDetail extends BaseComponent {
return;
}
if (this.isRantBlocked()) {
this.setHtml(`
<div class="rant-detail-blocked">
<header class="detail-header">
<button class="back-btn" aria-label="Go back">
<svg viewBox="0 0 24 24" width="24" height="24">
<path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/>
</svg>
</button>
<h1 class="detail-title">Blocked User</h1>
</header>
<div class="blocked-message">
<svg viewBox="0 0 24 24" width="48" height="48">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zM4 12c0-4.42 3.58-8 8-8 1.85 0 3.55.63 4.9 1.69L5.69 16.9C4.63 15.55 4 13.85 4 12zm8 8c-1.85 0-3.55-.63-4.9-1.69L18.31 7.1C19.37 8.45 20 10.15 20 12c0 4.42-3.58 8-8 8z"/>
</svg>
<p>This rant is from a blocked user</p>
<p class="blocked-username">@${this.rantData.user_username}</p>
<button class="btn btn-secondary unblock-btn" data-username="${this.rantData.user_username}">Unblock User</button>
</div>
</div>
`);
return;
}
const rant = this.rantData;
const hasImage = rant.attached_image && typeof rant.attached_image === 'object';
const imageUrl = hasImage ? buildDevrantImageUrl(rant.attached_image.url) : null;
@@ -131,7 +172,7 @@ class RantDetail extends BaseComponent {
<section class="comments-section">
<comment-form rant-id="${rant.id}"></comment-form>
<div class="comments-list">
${this.comments.map(comment => `
${this.getFilteredComments().map(comment => `
<comment-item
comment-id="${comment.id}"
data-comment='${JSON.stringify(comment).replace(/'/g, '&#39;')}'>
@@ -155,14 +196,11 @@ class RantDetail extends BaseComponent {
}
initComments() {
const filteredComments = this.getFilteredComments();
const commentItems = this.$$('comment-item');
commentItems.forEach(item => {
const commentData = item.dataset.comment;
if (commentData) {
try {
const comment = JSON.parse(commentData.replace(/&#39;/g, "'"));
item.setComment(comment);
} catch (e) {}
commentItems.forEach((item, index) => {
if (filteredComments[index]) {
item.setComment(filteredComments[index]);
}
});
}
@@ -171,6 +209,11 @@ class RantDetail extends BaseComponent {
this.on(this, 'click', this.handleClick);
this.on(this, 'vote', this.handleVote);
this.on(this, 'comment-posted', this.handleCommentPosted);
window.addEventListener('rantii:block-change', this.blockChangeHandler);
}
onDisconnected() {
window.removeEventListener('rantii:block-change', this.blockChangeHandler);
}
handleClick(e) {
@@ -178,6 +221,7 @@ class RantDetail extends BaseComponent {
const username = e.target.closest('.author-username');
const avatar = e.target.closest('user-avatar');
const tag = e.target.closest('.tag');
const unblockBtn = e.target.closest('.unblock-btn');
if (backBtn) {
e.preventDefault();
@@ -185,6 +229,17 @@ class RantDetail extends BaseComponent {
return;
}
if (unblockBtn) {
e.preventDefault();
e.stopPropagation();
const usernameToUnblock = unblockBtn.dataset.username;
if (usernameToUnblock) {
this.getBlock()?.unblock(usernameToUnblock);
window.app?.toast?.success(`Unblocked @${usernameToUnblock}`);
}
return;
}
if (username || avatar) {
this.getRouter()?.goToUser(this.rantData.user_username);
return;
+21 -6
View File
@@ -20,11 +20,22 @@ class RantFeed extends BaseComponent {
this.hasMore = true;
this.sort = this.getAttr('sort') || 'recent';
this.feedType = this.getAttr('feed-type') || 'rants';
this.blockChangeHandler = () => this.render();
this.render();
this.bindEvents();
}
getBlock() {
return window.app?.block;
}
getFilteredRants() {
const block = this.getBlock();
if (!block) return this.rants;
return block.filterRants(this.rants);
}
async load(reset = false) {
if (this.isLoading) return;
if (!reset && !this.hasMore) return;
@@ -98,7 +109,9 @@ class RantFeed extends BaseComponent {
render() {
this.addClass('rant-feed');
if (this.rants.length === 0 && !this.isLoading) {
const filteredRants = this.getFilteredRants();
if (filteredRants.length === 0 && !this.isLoading) {
this.setHtml(`
<div class="feed-empty">
<p>No rants found</p>
@@ -116,7 +129,7 @@ class RantFeed extends BaseComponent {
</div>
</div>
<div class="feed-list">
${this.rants.map(rant => `
${filteredRants.map(rant => `
<rant-card rant-id="${rant.id}"></rant-card>
`).join('')}
</div>
@@ -132,14 +145,14 @@ class RantFeed extends BaseComponent {
` : ''}
`);
this.initRantCards();
this.initRantCards(filteredRants);
}
initRantCards() {
initRantCards(filteredRants) {
const cards = this.$$('rant-card');
cards.forEach((card, index) => {
if (this.rants[index]) {
card.setRant(this.rants[index]);
if (filteredRants[index]) {
card.setRant(filteredRants[index]);
}
});
}
@@ -157,6 +170,7 @@ class RantFeed extends BaseComponent {
bindEvents() {
this.on(this, 'click', this.handleClick);
this.setupInfiniteScroll();
window.addEventListener('rantii:block-change', this.blockChangeHandler);
}
handleClick(e) {
@@ -203,6 +217,7 @@ class RantFeed extends BaseComponent {
}
this.isLoading = false;
this.hasMore = true;
window.removeEventListener('rantii:block-change', this.blockChangeHandler);
}
onAttributeChanged(name, oldValue, newValue) {
+49
View File
@@ -17,10 +17,21 @@ class UserProfile extends BaseComponent {
this.profileData = null;
this.isLoading = false;
this.activeTab = 'rants';
this.blockChangeHandler = () => this.render();
this.render();
this.bindEvents();
}
getBlock() {
return window.app?.block;
}
isUserBlocked() {
const block = this.getBlock();
if (!block || !this.profileData) return false;
return block.isBlocked(this.profileData.username);
}
async load(username) {
if (!username) return;
@@ -64,6 +75,8 @@ class UserProfile extends BaseComponent {
const profile = this.profileData;
const rants = profile.content?.content?.rants || [];
const comments = profile.content?.content?.comments || [];
const isBlocked = this.isUserBlocked();
const isOwnProfile = window.app?.auth?.getUsername()?.toLowerCase() === profile.username.toLowerCase();
this.addClass('user-profile');
@@ -84,8 +97,21 @@ class UserProfile extends BaseComponent {
<div class="profile-info">
<h1 class="profile-username">${profile.username}</h1>
<div class="profile-score">+${profile.score}</div>
${!isOwnProfile ? `
<button class="btn btn-small ${isBlocked ? 'btn-secondary' : 'btn-danger'} block-user-btn" data-username="${profile.username}">
${isBlocked ? 'Unblock' : 'Block'}
</button>
` : ''}
</div>
</header>
${isBlocked ? `
<div class="profile-blocked-banner">
<svg viewBox="0 0 24 24" width="20" height="20">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zM4 12c0-4.42 3.58-8 8-8 1.85 0 3.55.63 4.9 1.69L5.69 16.9C4.63 15.55 4 13.85 4 12zm8 8c-1.85 0-3.55-.63-4.9-1.69L18.31 7.1C19.37 8.45 20 10.15 20 12c0 4.42-3.58 8-8 8z"/>
</svg>
<span>This user is blocked</span>
</div>
` : ''}
<section class="profile-details">
${profile.about ? `
<div class="detail-item">
@@ -179,13 +205,36 @@ class UserProfile extends BaseComponent {
bindEvents() {
this.on(this, 'click', this.handleClick);
window.addEventListener('rantii:block-change', this.blockChangeHandler);
}
onDisconnected() {
window.removeEventListener('rantii:block-change', this.blockChangeHandler);
}
handleClick(e) {
const blockBtn = e.target.closest('.block-user-btn');
const backBtn = e.target.closest('.back-btn');
const tab = e.target.closest('.tab');
const profileComment = e.target.closest('.profile-comment');
if (blockBtn) {
e.preventDefault();
e.stopPropagation();
const username = blockBtn.dataset.username;
if (username) {
const block = this.getBlock();
if (block?.isBlocked(username)) {
block.unblock(username);
window.app?.toast?.success(`Unblocked @${username}`);
} else {
block?.block(username);
window.app?.toast?.success(`Blocked @${username}`);
}
}
return;
}
if (backBtn) {
e.preventDefault();
window.history.back();