feat(topbar): add customizable visibility controls for directories and subscriptions

This commit is contained in:
plebeius
2025-11-07 18:20:11 +01:00
parent bae3d76c6f
commit 669bb5b181
16 changed files with 767 additions and 46 deletions
@@ -0,0 +1 @@
export { default } from './topbar-edit-modal';
@@ -0,0 +1,142 @@
.backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.25);
z-index: 999;
}
.topbarEditDialog {
position: absolute;
width: 600px;
max-height: 80vh;
top: 50px;
left: 50%;
margin-left: -300px;
background-color: var(--settings-modal-background-color);
border-top: var(--settings-modal-border-top);
border-right: var(--settings-modal-border-right);
border-bottom: var(--settings-modal-border-bottom);
border-left: var(--settings-modal-border-left);
box-shadow: rgba(0, 0, 0, 0.1) 2px 2px 0px 1px;
font-size: var(--body-font-size);
line-height: 16.003px;
z-index: 1000;
display: flex;
flex-direction: column;
}
@media (max-width: 600px) {
.topbarEditDialog {
width: calc(100% - 20px);
margin-left: -50%;
left: 50%;
max-width: 600px;
}
}
.hd {
position: relative;
padding: 4px 6px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: var(--settings-modal-header-border-bottom);
}
.hd h2 {
margin: 0;
padding: 0;
font-size: 131%;
font-weight: bold;
}
.closeButton {
all: unset;
cursor: pointer;
width: 16px;
height: 16px;
position: absolute;
top: 4px;
right: 4px;
background-image: var(--settings-modal-close-button-background-image);
background-size: 100%;
background-position: center;
background-repeat: no-repeat;
image-rendering: pixelated;
}
.bd {
padding: 10px;
overflow-y: auto;
flex: 1;
}
.section {
margin-bottom: 20px;
}
.section:last-child {
margin-bottom: 0;
}
.section h3 {
margin: 0 0 8px 0;
padding: 0;
font-size: calc(var(--body-font-size) * 1.077);
font-weight: bold;
letter-spacing: 0.3px;
}
.directoryInput {
width: 100%;
padding: 4px;
font-size: var(--body-font-size);
border: var(--reply-modal-field-input-border, 1px solid #aaa);
outline: none;
font-family: var(--body-font-family, arial, helvetica, sans-serif);
}
.directoryInput:focus {
border: var(--reply-modal-field-input-border-focus, 1px solid #ea8);
}
.checkboxGroup {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
.checkboxItem {
display: flex;
align-items: center;
gap: 4px;
}
.checkboxItem input[type='checkbox'] {
cursor: pointer;
}
.checkboxItem label {
cursor: pointer;
font-size: var(--body-font-size);
user-select: none;
}
.topbarEditFooter {
padding: 10px;
display: flex;
justify-content: center;
gap: 10px;
border-top: var(--settings-modal-header-border-bottom);
}
.topbarEditFooter button {
padding: 4px 12px;
cursor: pointer;
font-size: var(--body-font-size);
}
@@ -0,0 +1,139 @@
import { useEffect, useState } from 'react';
import { useAccount } from '@plebbit/plebbit-react-hooks';
import Plebbit from '@plebbit/plebbit-js';
import useTopbarEditModalStore from '../../stores/use-topbar-edit-modal-store';
import useTopbarVisibilityStore from '../../stores/use-topbar-visibility-store';
import { getAllBoardCodes } from '../../constants/board-codes';
import { getBoardPath } from '../../lib/utils/route-utils';
import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
import styles from './topbar-edit-modal.module.css';
const TopbarEditModal = () => {
const { showModal, closeTopbarEditModal } = useTopbarEditModalStore();
const { visibleDirectories, visibleSubscriptions, setDirectoryVisibility, setSubscriptionVisibility } = useTopbarVisibilityStore();
const account = useAccount();
const subscriptions = account?.subscriptions || [];
const defaultSubplebbits = useDefaultSubplebbits();
// Convert visible directories set to space-separated string for input
const directoriesToString = (dirs: Set<string>): string => {
return Array.from(dirs).sort().join(' ');
};
// Convert space-separated string to set of directory codes
const stringToDirectories = (str: string): Set<string> => {
const codes = str
.trim()
.split(/\s+/)
.filter((code) => code.length > 0)
.map((code) => code.toLowerCase());
return new Set(codes);
};
// Local state for text input (will be saved on Save click)
const [localDirectoryInput, setLocalDirectoryInput] = useState<string>(directoriesToString(visibleDirectories));
const [localSubscriptionVisibility, setLocalSubscriptionVisibility] = useState<Set<string>>(visibleSubscriptions);
// Update local state when modal opens or store changes
useEffect(() => {
if (showModal) {
setLocalDirectoryInput(directoriesToString(visibleDirectories));
setLocalSubscriptionVisibility(new Set(visibleSubscriptions));
}
}, [showModal, visibleDirectories, visibleSubscriptions]);
if (!showModal) {
return null;
}
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target === e.currentTarget) {
closeTopbarEditModal();
}
};
const handleSubscriptionToggle = (address: string) => {
const newSet = new Set(localSubscriptionVisibility);
if (newSet.has(address)) {
newSet.delete(address);
} else {
newSet.add(address);
}
setLocalSubscriptionVisibility(newSet);
};
const handleSave = () => {
// Parse directory input and apply changes
const inputDirectories = stringToDirectories(localDirectoryInput);
const allCodes = getAllBoardCodes();
// Hide all directories first, then show only the ones in the input
allCodes.forEach((code) => {
setDirectoryVisibility(code, inputDirectories.has(code));
});
// Apply subscription visibility changes
subscriptions.forEach((address: string) => {
setSubscriptionVisibility(address, localSubscriptionVisibility.has(address));
});
closeTopbarEditModal();
};
const handleCancel = () => {
// Reset local state to match store
setLocalDirectoryInput(directoriesToString(visibleDirectories));
setLocalSubscriptionVisibility(new Set(visibleSubscriptions));
closeTopbarEditModal();
};
return (
<div className={styles.backdrop} onClick={handleBackdropClick}>
<div className={styles.topbarEditDialog}>
<div className={styles.hd}>
<h2>Custom Board List</h2>
<button className={styles.closeButton} onClick={handleCancel} title='Close' />
</div>
<div className={styles.bd}>
<div className={styles.section}>
<h3>Directory Boards</h3>
<p>Enter directory codes separated by spaces (e.g., "jp tg mu"):</p>
<input
type='text'
className={styles.directoryInput}
placeholder='Example: jp tg mu'
value={localDirectoryInput}
onChange={(e) => setLocalDirectoryInput(e.target.value)}
/>
</div>
{subscriptions.length > 0 && (
<div className={styles.section}>
<h3>Subscriptions</h3>
<p>Select which subscriptions to show in the topbar:</p>
<div className={styles.checkboxGroup}>
{subscriptions.map((address: string) => {
const boardPath = getBoardPath(address, defaultSubplebbits);
const displayText = address.endsWith('.eth') || address.endsWith('.sol') ? address : Plebbit.getShortAddress(address);
const isChecked = localSubscriptionVisibility.has(address);
return (
<div key={address} className={styles.checkboxItem}>
<input type='checkbox' id={`subscription-${address}`} checked={isChecked} onChange={() => handleSubscriptionToggle(address)} />
<label htmlFor={`subscription-${address}`}>{boardPath || displayText}</label>
</div>
);
})}
</div>
</div>
)}
</div>
<div className={styles.topbarEditFooter}>
<button onClick={handleSave}>Save</button>
<button onClick={handleCancel}>Cancel</button>
</div>
</div>
</div>
);
};
export default TopbarEditModal;
@@ -0,0 +1,13 @@
// Types for topbar edit modal component
export interface BoardCodeCheckbox {
code: string;
label: string;
checked: boolean;
}
export interface SubscriptionCheckbox {
address: string;
displayName: string;
checked: boolean;
}