SidebarAccordion.svelte raw

   1  <script>
   2      import { createEventDispatcher } from 'svelte';
   3      import { activeView, expandedSection, userMenuOpen } from './stores.js';
   4      import { totalUnreadDMs, totalUnreadChannels } from './chatStores.js';
   5      import { totalUnreadCount } from './notificationStores.js';
   6  
   7      export let isLoggedIn = false;
   8      export let userProfile = null;
   9      export let userPubkey = "";
  10      export let currentEffectiveRole = "";
  11      export let version = "";
  12      export let mobileOpen = false;
  13  
  14      // Admin feature flags
  15      export let aclMode = "";
  16      export let sprocketEnabled = false;
  17      export let policyEnabled = false;
  18      export let nrcEnabled = false;
  19      export let blossomEnabled = true;
  20      export let isOrlyRelay = true;
  21  
  22      const dispatch = createEventDispatcher();
  23  
  24      // Navigation structure
  25      const sections = [
  26          {
  27              id: "feed",
  28              icon: "⚡",
  29              label: "Feed",
  30              children: null, // no children = direct nav
  31          },
  32          {
  33              id: "chat",
  34              icon: "💬",
  35              label: "Chat",
  36              children: [
  37                  { id: "chat-inbox", label: "Inbox" },
  38                  { id: "chat-channels", label: "Channels" },
  39              ],
  40          },
  41          {
  42              id: "library",
  43              icon: "📚",
  44              label: "Library",
  45              children: [
  46                  { id: "library-my", label: "My Library" },
  47                  { id: "library-bookmarks", label: "Bookmarks" },
  48                  { id: "library-new", label: "New" },
  49              ],
  50          },
  51      ];
  52  
  53      // Build admin section dynamically based on permissions
  54      $: adminChildren = buildAdminChildren(isLoggedIn, currentEffectiveRole, aclMode,
  55          sprocketEnabled, policyEnabled, nrcEnabled, blossomEnabled, isOrlyRelay);
  56  
  57      function buildAdminChildren(loggedIn, role, acl, sprocket, policy, nrc, blossom, orly) {
  58          if (!loggedIn) return [];
  59          const items = [];
  60          items.push({ id: "admin-export", label: "Export" });
  61          if (role === "admin" || role === "owner") {
  62              items.push({ id: "admin-import", label: "Import" });
  63          }
  64          if (role === "read" || role === "write" || role === "admin" || role === "owner") {
  65              items.push({ id: "admin-events", label: "Events" });
  66          }
  67          if (blossom) {
  68              items.push({ id: "admin-blossom", label: "Blossom" });
  69          }
  70          if (role !== "read") {
  71              items.push({ id: "admin-compose", label: "Compose" });
  72          }
  73          items.push({ id: "admin-recovery", label: "Recovery" });
  74          if (role === "owner" && orly) {
  75              if (acl === "managed") items.push({ id: "admin-managed-acl", label: "Managed ACL" });
  76              if (acl === "curating") items.push({ id: "admin-curation", label: "Curation" });
  77              if (sprocket) items.push({ id: "admin-sprocket", label: "Sprocket" });
  78              if (policy) items.push({ id: "admin-policy", label: "Policy" });
  79              if (nrc) items.push({ id: "admin-relay-connect", label: "Relay Connect" });
  80              items.push({ id: "admin-logs", label: "Logs" });
  81          }
  82          return items;
  83      }
  84  
  85      $: allSections = [
  86          ...sections,
  87          ...(adminChildren.length > 0 ? [{
  88              id: "admin",
  89              icon: "⚙️",
  90              label: "Admin",
  91              children: adminChildren,
  92          }] : []),
  93      ];
  94  
  95      function toggleSection(sectionId) {
  96          expandedSection.update(current => current === sectionId ? null : sectionId);
  97      }
  98  
  99      function navigate(viewId) {
 100          activeView.set(viewId);
 101          dispatch('navigate', viewId);
 102          // Close mobile menu on navigation
 103          if (mobileOpen) dispatch('closeMobileMenu');
 104      }
 105  
 106      function handleSectionClick(section) {
 107          if (section.children) {
 108              toggleSection(section.id);
 109          } else {
 110              navigate(section.id);
 111          }
 112      }
 113  
 114      function handleChildClick(childId) {
 115          navigate(childId);
 116      }
 117  
 118      function getUnreadBadge(sectionId) {
 119          if (sectionId === "chat") return $totalUnreadDMs + $totalUnreadChannels;
 120          return 0;
 121      }
 122  
 123      function getChildUnreadBadge(childId) {
 124          if (childId === "chat-inbox") return $totalUnreadDMs;
 125          if (childId === "chat-channels") return $totalUnreadChannels;
 126          return 0;
 127      }
 128  
 129      function handleUserClick() {
 130          userMenuOpen.update(v => !v);
 131          dispatch('toggleUserMenu');
 132      }
 133  
 134      function handleLogoClick() {
 135          dispatch('showAbout');
 136      }
 137  
 138      // Truncate display name
 139      function displayName(profile, pubkey) {
 140          if (profile?.name) return profile.name;
 141          if (profile?.display_name) return profile.display_name;
 142          if (pubkey) return pubkey.slice(0, 8) + '...';
 143          return 'Anonymous';
 144      }
 145  </script>
 146  
 147  <nav class="sidebar-accordion" class:mobile-open={mobileOpen}>
 148      {#if mobileOpen}
 149          <div class="mobile-overlay" on:click={() => dispatch('closeMobileMenu')}></div>
 150      {/if}
 151  
 152      <div class="sidebar-content">
 153          <!-- User avatar / login button at top -->
 154          <div class="sidebar-user">
 155              {#if isLoggedIn}
 156                  <button class="user-button" on:click={handleUserClick}>
 157                      {#if userProfile?.picture}
 158                          <img src={userProfile.picture} alt="avatar" class="user-avatar" />
 159                      {:else}
 160                          <div class="user-avatar-placeholder">
 161                              {displayName(userProfile, userPubkey).charAt(0).toUpperCase()}
 162                          </div>
 163                      {/if}
 164                      <span class="user-name">{displayName(userProfile, userPubkey)}</span>
 165                  </button>
 166              {:else}
 167                  <button class="login-button" on:click={() => dispatch('openLoginModal')}>
 168                      Log in
 169                  </button>
 170              {/if}
 171          </div>
 172  
 173          <!-- Navigation sections -->
 174          <div class="nav-sections">
 175              {#each allSections as section}
 176                  <div class="nav-section" class:expanded={$expandedSection === section.id}>
 177                      <button
 178                          class="section-header"
 179                          class:active={$activeView === section.id || (section.children && section.children.some(c => $activeView === c.id))}
 180                          on:click={() => handleSectionClick(section)}
 181                      >
 182                          <span class="section-icon">{section.icon}</span>
 183                          <span class="section-label">{section.label}</span>
 184                          {#if section.children}
 185                              <span class="section-chevron">{$expandedSection === section.id ? '▾' : '▸'}</span>
 186                          {/if}
 187                          {#if getUnreadBadge(section.id) > 0}
 188                              <span class="unread-badge">{getUnreadBadge(section.id)}</span>
 189                          {/if}
 190                      </button>
 191  
 192                      {#if section.children && $expandedSection === section.id}
 193                          <div class="section-children">
 194                              {#each section.children as child}
 195                                  <button
 196                                      class="child-item"
 197                                      class:active={$activeView === child.id}
 198                                      on:click={() => handleChildClick(child.id)}
 199                                  >
 200                                      <span class="child-label">{child.label}</span>
 201                                      {#if getChildUnreadBadge(child.id) > 0}
 202                                          <span class="unread-badge small">{getChildUnreadBadge(child.id)}</span>
 203                                      {/if}
 204                                  </button>
 205                              {/each}
 206                              <div class="section-boundary"></div>
 207                          </div>
 208                      {/if}
 209                  </div>
 210              {/each}
 211          </div>
 212  
 213          <!-- Logo at bottom -->
 214          <div class="sidebar-footer">
 215              <button class="logo-button" on:click={handleLogoClick} title="About smesh">
 216                  <span class="logo-text">smesh</span>
 217                  {#if version}
 218                      <span class="version-text">{version}</span>
 219                  {/if}
 220              </button>
 221          </div>
 222      </div>
 223  </nav>
 224  
 225  <style>
 226      .sidebar-accordion {
 227          position: fixed;
 228          top: 3em;
 229          left: 0;
 230          bottom: 0;
 231          width: 200px;
 232          background: var(--sidebar-bg);
 233          border-right: 1px solid var(--border-color);
 234          display: flex;
 235          flex-direction: column;
 236          z-index: 100;
 237          transition: transform 0.2s ease;
 238      }
 239  
 240      .sidebar-content {
 241          display: flex;
 242          flex-direction: column;
 243          height: 100%;
 244          overflow-y: auto;
 245      }
 246  
 247      /* User section */
 248      .sidebar-user {
 249          padding: 0.75em;
 250          border-bottom: 1px solid var(--border-color);
 251      }
 252  
 253      .user-button {
 254          display: flex;
 255          align-items: center;
 256          gap: 0.5em;
 257          width: 100%;
 258          padding: 0.5em;
 259          background: none;
 260          border: none;
 261          border-radius: 8px;
 262          cursor: pointer;
 263          color: var(--text-color);
 264          transition: background 0.15s;
 265      }
 266  
 267      .user-button:hover {
 268          background: var(--button-hover-bg);
 269      }
 270  
 271      .user-avatar {
 272          width: 32px;
 273          height: 32px;
 274          border-radius: 50%;
 275          object-fit: cover;
 276      }
 277  
 278      .user-avatar-placeholder {
 279          width: 32px;
 280          height: 32px;
 281          border-radius: 50%;
 282          background: var(--primary);
 283          color: #000;
 284          display: flex;
 285          align-items: center;
 286          justify-content: center;
 287          font-weight: bold;
 288          font-size: 0.9rem;
 289      }
 290  
 291      .user-name {
 292          font-size: 0.85rem;
 293          font-weight: 500;
 294          overflow: hidden;
 295          text-overflow: ellipsis;
 296          white-space: nowrap;
 297      }
 298  
 299      .login-button {
 300          width: 100%;
 301          padding: 0.6em;
 302          background: var(--primary);
 303          color: #000;
 304          border: none;
 305          border-radius: 6px;
 306          cursor: pointer;
 307          font-weight: 600;
 308          font-size: 0.85rem;
 309          transition: filter 0.15s;
 310      }
 311  
 312      .login-button:hover {
 313          filter: brightness(0.9);
 314      }
 315  
 316      /* Navigation sections */
 317      .nav-sections {
 318          flex: 1;
 319          padding: 0.5em 0;
 320      }
 321  
 322      .nav-section {
 323          margin: 0;
 324      }
 325  
 326      .section-header {
 327          display: flex;
 328          align-items: center;
 329          gap: 0.5em;
 330          width: 100%;
 331          padding: 0.6em 0.75em;
 332          background: none;
 333          border: none;
 334          cursor: pointer;
 335          color: var(--text-color);
 336          font-size: 0.85rem;
 337          font-weight: 500;
 338          transition: background 0.15s;
 339          text-align: left;
 340      }
 341  
 342      .section-header:hover {
 343          background: var(--button-hover-bg);
 344      }
 345  
 346      .section-header.active {
 347          color: var(--primary);
 348          background: var(--primary-bg);
 349      }
 350  
 351      .section-icon {
 352          font-size: 1rem;
 353          width: 1.5em;
 354          text-align: center;
 355      }
 356  
 357      .section-label {
 358          flex: 1;
 359      }
 360  
 361      .section-chevron {
 362          font-size: 0.7rem;
 363          color: var(--text-muted);
 364      }
 365  
 366      .unread-badge {
 367          background: var(--primary);
 368          color: #000;
 369          font-size: 0.65rem;
 370          font-weight: 700;
 371          padding: 0.1em 0.4em;
 372          border-radius: 10px;
 373          min-width: 1.2em;
 374          text-align: center;
 375      }
 376  
 377      .unread-badge.small {
 378          font-size: 0.6rem;
 379          padding: 0.05em 0.35em;
 380      }
 381  
 382      /* Children */
 383      .section-children {
 384          padding-left: 0;
 385          background: var(--primary-bg);
 386      }
 387  
 388      .child-item {
 389          display: flex;
 390          align-items: center;
 391          gap: 0.5em;
 392          width: 100%;
 393          padding: 0.45em 0.75em 0.45em 2.75em;
 394          background: none;
 395          border: none;
 396          cursor: pointer;
 397          color: var(--text-color);
 398          font-size: 0.8rem;
 399          transition: background 0.15s;
 400          text-align: left;
 401      }
 402  
 403      .child-item:hover {
 404          background: var(--button-hover-bg);
 405      }
 406  
 407      .child-item.active {
 408          color: var(--primary);
 409          font-weight: 600;
 410      }
 411  
 412      .child-label {
 413          flex: 1;
 414      }
 415  
 416      .section-boundary {
 417          height: 1px;
 418          background: var(--border-color);
 419          margin: 0.25em 0.75em;
 420      }
 421  
 422      /* Footer */
 423      .sidebar-footer {
 424          padding: 0.5em;
 425          border-top: 1px solid var(--border-color);
 426          text-align: right;
 427      }
 428  
 429      .logo-button {
 430          background: none;
 431          border: none;
 432          cursor: pointer;
 433          padding: 0.4em 0.6em;
 434          border-radius: 6px;
 435          transition: background 0.15s;
 436      }
 437  
 438      .logo-button:hover {
 439          background: var(--button-hover-bg);
 440      }
 441  
 442      .logo-text {
 443          font-size: 0.85rem;
 444          font-weight: 700;
 445          color: var(--primary);
 446          letter-spacing: 0.05em;
 447      }
 448  
 449      .version-text {
 450          display: block;
 451          font-size: 0.6rem;
 452          color: var(--text-muted);
 453          margin-top: 0.1em;
 454      }
 455  
 456      /* Mobile */
 457      .mobile-overlay {
 458          position: fixed;
 459          top: 0;
 460          left: 0;
 461          right: 0;
 462          bottom: 0;
 463          background: rgba(0, 0, 0, 0.5);
 464          z-index: -1;
 465      }
 466  
 467      @media (max-width: 1280px) {
 468          .sidebar-accordion {
 469              width: 60px;
 470          }
 471  
 472          .section-label,
 473          .section-chevron,
 474          .user-name,
 475          .child-label,
 476          .version-text,
 477          .logo-text {
 478              display: none;
 479          }
 480  
 481          .section-header {
 482              justify-content: center;
 483              padding: 0.6em;
 484          }
 485  
 486          .section-icon {
 487              width: auto;
 488          }
 489  
 490          .section-children {
 491              display: none;
 492          }
 493  
 494          .sidebar-user {
 495              padding: 0.5em;
 496          }
 497  
 498          .user-button {
 499              justify-content: center;
 500              padding: 0.5em;
 501          }
 502  
 503          .user-avatar-placeholder,
 504          .user-avatar {
 505              width: 28px;
 506              height: 28px;
 507          }
 508  
 509          .sidebar-footer {
 510              text-align: center;
 511          }
 512      }
 513  
 514      @media (max-width: 640px) {
 515          .sidebar-accordion {
 516              width: 250px;
 517              transform: translateX(-100%);
 518              top: 0;
 519              z-index: 1000;
 520          }
 521  
 522          .sidebar-accordion.mobile-open {
 523              transform: translateX(0);
 524          }
 525  
 526          .section-label,
 527          .section-chevron,
 528          .user-name,
 529          .child-label,
 530          .version-text,
 531          .logo-text {
 532              display: inline;
 533          }
 534  
 535          .section-header {
 536              justify-content: flex-start;
 537              padding: 0.6em 0.75em;
 538          }
 539  
 540          .section-children {
 541              display: block;
 542          }
 543  
 544          .sidebar-footer {
 545              text-align: right;
 546          }
 547      }
 548  </style>
 549