Added blocking service.
Some checks failed
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
Some checks failed
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:
parent
2078c1115a
commit
3570ba5b99
157
css/components/block.css
Normal file
157
css/components/block.css
Normal file
@ -0,0 +1,157 @@
|
||||
.blocked-users-manager {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.block-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.block-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.block-count {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.block-add-form {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.block-input {
|
||||
flex: 1;
|
||||
padding: 0.625rem 0.875rem;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-text);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.block-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px rgba(233, 69, 96, 0.2);
|
||||
}
|
||||
|
||||
.block-input::placeholder {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.block-add-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.blocked-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.blocked-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.blocked-item .blocked-username {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.blocked-empty {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.blocked-empty p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.block-actions {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.rant-detail-blocked {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.rant-detail-blocked .detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.blocked-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.blocked-message svg {
|
||||
color: var(--color-error);
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.blocked-message p {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.blocked-message .blocked-username {
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.blocked-message .unblock-btn {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.profile-blocked-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: rgba(248, 113, 113, 0.1);
|
||||
border: 1px solid var(--color-error);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-error);
|
||||
margin: 0 1rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.profile-blocked-banner svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.block-user-btn {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
@ -38,6 +38,7 @@
|
||||
<link rel="stylesheet" href="css/components/notification.css">
|
||||
<link rel="stylesheet" href="css/components/form.css">
|
||||
<link rel="stylesheet" href="css/components/pages.css">
|
||||
<link rel="stylesheet" href="css/components/block.css">
|
||||
<link rel="stylesheet" href="lib/highlight.css">
|
||||
|
||||
<script src="lib/marked.min.js"></script>
|
||||
|
||||
@ -10,6 +10,7 @@ import { StorageService } from './services/storage.js';
|
||||
import { AuthService } from './services/auth.js';
|
||||
import { Router } from './services/router.js';
|
||||
import { ThemeService } from './services/theme.js';
|
||||
import { BlockService } from './services/block.js';
|
||||
import { markdownRenderer } from './utils/markdown.js';
|
||||
|
||||
import { BaseComponent } from './components/base-component.js';
|
||||
@ -33,6 +34,7 @@ import { CommentForm } from './components/comment-form.js';
|
||||
import { UserProfile } from './components/user-profile.js';
|
||||
import { NotificationList, NotificationItem } from './components/notification-list.js';
|
||||
import { PostForm, PostModal } from './components/post-form.js';
|
||||
import { BlockedUsersManager } from './components/blocked-users-manager.js';
|
||||
|
||||
import { HomePage } from './pages/home-page.js';
|
||||
import { RantPage } from './pages/rant-page.js';
|
||||
@ -52,6 +54,7 @@ class Application {
|
||||
this.auth = new AuthService(this.api, this.storage);
|
||||
this.router = new Router();
|
||||
this.theme = new ThemeService(this.storage);
|
||||
this.block = new BlockService(this.storage);
|
||||
this.toast = null;
|
||||
this.header = null;
|
||||
this.nav = null;
|
||||
@ -90,6 +93,7 @@ class Application {
|
||||
async init() {
|
||||
await this.auth.init();
|
||||
this.theme.init();
|
||||
this.block.init();
|
||||
await markdownRenderer.init();
|
||||
|
||||
this.setupToast();
|
||||
|
||||
111
js/components/blocked-users-manager.js
Normal file
111
js/components/blocked-users-manager.js
Normal 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 };
|
||||
@ -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, ''')}'>
|
||||
</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(/'/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() {
|
||||
|
||||
@ -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, ''')}'>
|
||||
@ -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(/'/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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -63,6 +63,10 @@ class SettingsPage extends BaseComponent {
|
||||
<h2>Appearance</h2>
|
||||
<theme-selector></theme-selector>
|
||||
</section>
|
||||
<section class="settings-section">
|
||||
<h2>Blocked Users</h2>
|
||||
<blocked-users-manager></blocked-users-manager>
|
||||
</section>
|
||||
<section class="settings-section">
|
||||
<h2>About</h2>
|
||||
<div class="about-info">
|
||||
|
||||
@ -22,11 +22,27 @@ class AuthService {
|
||||
username: authData.username
|
||||
};
|
||||
this.notifyAuthChange();
|
||||
this.fetchAndUpdateUsername(authData);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async fetchAndUpdateUsername(authData) {
|
||||
if (!authData.userId) return;
|
||||
try {
|
||||
const profileResult = await this.api.getProfile(authData.userId);
|
||||
if (profileResult?.success && profileResult.profile?.username) {
|
||||
this.currentUser.username = profileResult.profile.username;
|
||||
if (authData.username !== profileResult.profile.username) {
|
||||
authData.username = profileResult.profile.username;
|
||||
this.storage.setAuth(authData);
|
||||
}
|
||||
this.notifyAuthChange();
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async login(username, password, remember = true) {
|
||||
const result = await this.api.login(username, password);
|
||||
if (result.success) {
|
||||
|
||||
95
js/services/block.js
Normal file
95
js/services/block.js
Normal file
@ -0,0 +1,95 @@
|
||||
class BlockService {
|
||||
constructor(storageService) {
|
||||
this.storage = storageService;
|
||||
this.blockedUsers = new Set();
|
||||
this.onBlockChange = null;
|
||||
}
|
||||
|
||||
init() {
|
||||
const blocked = this.storage.getBlockedUsers();
|
||||
this.blockedUsers = new Set(blocked.map(u => u.toLowerCase()));
|
||||
}
|
||||
|
||||
isBlocked(username) {
|
||||
if (!username) return false;
|
||||
return this.blockedUsers.has(username.toLowerCase());
|
||||
}
|
||||
|
||||
block(username) {
|
||||
if (!username) return false;
|
||||
const normalized = username.toLowerCase();
|
||||
if (this.blockedUsers.has(normalized)) return false;
|
||||
this.blockedUsers.add(normalized);
|
||||
this.save();
|
||||
this.notifyChange();
|
||||
return true;
|
||||
}
|
||||
|
||||
unblock(username) {
|
||||
if (!username) return false;
|
||||
const normalized = username.toLowerCase();
|
||||
if (!this.blockedUsers.has(normalized)) return false;
|
||||
this.blockedUsers.delete(normalized);
|
||||
this.save();
|
||||
this.notifyChange();
|
||||
return true;
|
||||
}
|
||||
|
||||
toggle(username) {
|
||||
if (this.isBlocked(username)) {
|
||||
return this.unblock(username);
|
||||
}
|
||||
return this.block(username);
|
||||
}
|
||||
|
||||
getBlockedUsers() {
|
||||
return Array.from(this.blockedUsers).sort();
|
||||
}
|
||||
|
||||
getBlockedCount() {
|
||||
return this.blockedUsers.size;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.blockedUsers.clear();
|
||||
this.save();
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
save() {
|
||||
this.storage.setBlockedUsers(Array.from(this.blockedUsers));
|
||||
}
|
||||
|
||||
notifyChange() {
|
||||
if (this.onBlockChange) {
|
||||
this.onBlockChange(this.getBlockedUsers());
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent('rantii:block-change', {
|
||||
detail: { blockedUsers: this.getBlockedUsers() }
|
||||
}));
|
||||
}
|
||||
|
||||
setBlockChangeCallback(callback) {
|
||||
this.onBlockChange = callback;
|
||||
}
|
||||
|
||||
filterRants(rants) {
|
||||
if (!rants || !Array.isArray(rants)) return [];
|
||||
return rants.filter(rant => !this.isBlocked(rant.user_username));
|
||||
}
|
||||
|
||||
filterComments(comments) {
|
||||
if (!comments || !Array.isArray(comments)) return [];
|
||||
return comments.filter(comment => !this.isBlocked(comment.user_username));
|
||||
}
|
||||
|
||||
filterNotifications(notifications) {
|
||||
if (!notifications || !Array.isArray(notifications)) return [];
|
||||
return notifications.filter(notif => {
|
||||
const username = notif.username?.name || notif.user_username?.name || notif.name;
|
||||
return !this.isBlocked(username);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { BlockService };
|
||||
@ -133,6 +133,37 @@ class StorageService {
|
||||
return this.remove(`draft_comment_${rantId}`);
|
||||
}
|
||||
|
||||
getBlockedUsers() {
|
||||
return this.get('blocked_users', []);
|
||||
}
|
||||
|
||||
setBlockedUsers(users) {
|
||||
return this.set('blocked_users', users);
|
||||
}
|
||||
|
||||
addBlockedUser(username) {
|
||||
const blocked = this.getBlockedUsers();
|
||||
const normalized = username.toLowerCase();
|
||||
if (!blocked.includes(normalized)) {
|
||||
blocked.push(normalized);
|
||||
return this.setBlockedUsers(blocked);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
removeBlockedUser(username) {
|
||||
const blocked = this.getBlockedUsers();
|
||||
const normalized = username.toLowerCase();
|
||||
const filtered = blocked.filter(u => u !== normalized);
|
||||
return this.setBlockedUsers(filtered);
|
||||
}
|
||||
|
||||
isUserBlocked(username) {
|
||||
if (!username) return false;
|
||||
const blocked = this.getBlockedUsers();
|
||||
return blocked.includes(username.toLowerCase());
|
||||
}
|
||||
|
||||
getCachedProfile(userId) {
|
||||
return this.get(`profile_cache_${userId}`, null);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user