aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/app.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js175
1 files changed, 59 insertions, 116 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js
index 703bd47..50a972b 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js
@@ -8,14 +8,16 @@ import { transfers, formatSpeed } from './transfers.js';
import * as downloads from './downloads.js';
import * as platform from './platform.js';
import { Icon } from './icon.js';
-import { FILE_ICONS, formatSize } from './file-utils.js';
+import { formatSize } from './file-utils.js';
import {
- HUB, navigate, session, getCachedGroupIndex, getAllCachedIndexes,
+ HUB, navigate, session, getCachedGroupIndex,
_storeBundleKey, _loadBundleKey, _clearKeyDB,
loadAuth, saveAuth, setAuth, setAuthChangeListener, ensureFreshToken, hubFetch,
refreshAccessToken,
} from './hub-client.js';
import { GroupPage } from './group-page.js';
+import { SearchPage, ConnectionPool } from './search-page.js';
+import { MusicPlayerBar } from './music-player.js';
import { GroupName } from './group-name.js';
import { APPS } from './apps.js';
@@ -1293,118 +1295,6 @@ function CreateGroupWizard({ token, username, onCreated, allowPublicGroups = tru
</div>`;
}
-
-// ── Search Page (cross-group file search) ───────────────────────────────────
-
-/**
- * "3 hours ago", in the reader's language.
- *
- * The search page needs it because its results come from a cache: a file that
- * was deleted an hour ago is still listed until the group is opened again, and
- * the honest thing is to say how old the answer is rather than to imply it is
- * live.
- */
-function formatAgo(ts) {
- if (!ts) return '';
- const rtf = new Intl.RelativeTimeFormat(getLocale(), { numeric: 'auto' });
- let delta = (ts - Date.now()) / 1000;
- const steps = [['second', 60], ['minute', 60], ['hour', 24],
- ['day', 7], ['week', 4.35], ['month', 12], ['year', Infinity]];
- for (const [unit, span] of steps) {
- if (Math.abs(delta) < span || span === Infinity) {
- return rtf.format(Math.round(delta), unit);
- }
- delta /= span;
- }
- return '';
-}
-
-function SearchPage() {
- const [query, setQuery] = useState('');
- const [results, setResults] = useState([]);
- const [searched, setSearched] = useState(false);
-
- const doSearch = useCallback(async (q) => {
- const term = q.trim().toLowerCase();
- if (!term) { setResults([]); setSearched(false); return; }
- const indexes = await getAllCachedIndexes();
- const hits = [];
- for (const idx of indexes) {
- for (const e of (idx.entries || [])) {
- if (e.name.toLowerCase().includes(term) ||
- (e.path && e.path.toLowerCase().includes(term))) {
- hits.push({ ...e, groupId: idx.groupId, groupName: idx.groupName,
- groupOwner: idx.groupOwner, syncedAt: idx.cachedAt });
- }
- }
- }
- setResults(hits);
- setSearched(true);
- }, []);
-
- const onInput = useCallback((e) => {
- const q = e.target.value;
- setQuery(q);
- doSearch(q);
- }, [doSearch]);
-
- return html`
- <div>
- <h2>${t('search.title')}</h2>
- <div class="file-toolbar" style="margin-bottom:16px">
- <input type="text" class="admin-search" style="width:100%"
- placeholder="${t('search.placeholder')}"
- value=${query} onInput=${onInput} autofocus />
- </div>
- ${searched && results.length === 0 && html`
- <p class="page-message">${t('search.no_results')}</p>
- `}
- ${results.length > 0 && html`
- <table class="file-table">
- <thead>
- <tr>
- <th></th>
- <th>${t('group.col_name')}</th>
- <th>${t('group.col_size')}</th>
- <th>${t('search.col_group')}</th>
- <th>${t('group.col_type')}</th>
- </tr>
- </thead>
- <tbody>
- ${results.map(r => html`
- <tr class="file-row" key=${r.id + r.groupId}>
- <td>${FILE_ICONS[r.type] || FILE_ICONS.other}</td>
- <td class="file-name">
- <a href="#/group/${r.groupId}" style="color:inherit">${r.name}</a>
- </td>
- <td class="file-size">${formatSize(r.size)}</td>
- <td>
- <a href="#/group/${r.groupId}" class="badge">${r.groupName
- ? html`<${GroupName} name=${r.groupName} owner=${r.groupOwner} inline=${true} />`
- : r.groupId.slice(0, 8)}</a>
- ${r.syncedAt && html`
- <div class="search-synced">
- ${t('search.synced', { ago: formatAgo(r.syncedAt) })}
- </div>
- `}
- </td>
- <td class="file-type">${r.type}</td>
- </tr>
- `)}
- </tbody>
- </table>
- <p class="settings-value" style="margin-top:8px">
- ${t('search.result_count', { n: results.length })}
- </p>
- `}
- ${!searched && html`
- <p class="page-message">${t('search.hint')}</p>
- `}
- <p class="settings-hint" style="margin-top:12px">${t('search.cache_note')}</p>
- </div>
- `;
-}
-
// ── Settings Page ───────────────────────────────────────────────────────────
const THEME_OPTIONS = ['light', 'dark', 'system'];
@@ -3010,6 +2900,48 @@ function App() {
const [hubInfo, setHubInfo] = useState(null);
const allowPublicGroups = !hubInfo || hubInfo.allow_public_groups !== false;
+ // -- Persistent music player (lifted from group-page.js) --
+ const [musicQueue, setMusicQueue] = useState(null);
+ const musicPoolRef = useRef(null);
+ const userRef = useRef(user);
+ userRef.current = user;
+ const groupTransportRef = useRef(null);
+
+ useEffect(() => {
+ musicPoolRef.current = new ConnectionPool(HUB);
+ return () => { if (musicPoolRef.current) musicPoolRef.current.closeAll(); };
+ }, []);
+
+ const getMusicConnection = useCallback(async (groupId) => {
+ const gt = groupTransportRef.current;
+ if (gt && gt.groupId === groupId) {
+ const tr = gt.transportRef.current;
+ if (tr && tr.connected) return { transport: tr, gek: gt.gekRef.current };
+ }
+ const u = userRef.current;
+ if (!u || !musicPoolRef.current) throw new Error('no connection');
+ const bundleKey = session.bundleKey || await _loadBundleKey();
+ if (bundleKey) session.bundleKey = bundleKey;
+ const conn = await musicPoolRef.current.connect(
+ groupId, u.token, bundleKey, u.username, u.userId);
+ return { transport: conn.transport, gek: conn.gek };
+ }, []);
+
+ const handlePlayQueue = useCallback((tracks, startIndex, source) => {
+ if (source && source.transportRef) {
+ groupTransportRef.current = {
+ groupId: source.groupId,
+ transportRef: source.transportRef,
+ gekRef: source.gekRef,
+ };
+ } else {
+ groupTransportRef.current = null;
+ }
+ setMusicQueue({ tracks, startIndex, nonce: Date.now() });
+ }, []);
+
+ const handleStopMusic = useCallback(() => setMusicQueue(null), []);
+
const resolved = resolveTheme(theme);
// Keep the session alive without anyone having to think about it.
@@ -3286,7 +3218,10 @@ function App() {
} else if (!user) {
page = html`<${LoginPage} />`;
} else if (route === '/search') {
- page = html`<${SearchPage} />`;
+ page = html`<${SearchPage}
+ token=${user.token} username=${user.username} userId=${user.userId}
+ groups=${groups} userPrefs=${userPrefs}
+ onPlayQueue=${handlePlayQueue} />`;
} else if (route === '/explore') {
page = html`<${ExplorePage} token=${user.token}
myGroupIds=${groups.map(g => g.id)}
@@ -3311,7 +3246,8 @@ function App() {
userPrefs=${userPrefs}
onRefreshAuth=${refreshAuth} onJoined=${dismissGroupNotifications}
onGroupUpdated=${updateGroup} onPresence=${notePresence}
- onLeft=${handleLeftGroup} />`;
+ onLeft=${handleLeftGroup}
+ onPlayQueue=${handlePlayQueue} onStopMusic=${handleStopMusic} />`;
} else if (route === '/admin') {
page = (user.role === 'moderator' || user.role === 'admin')
? html`<${AdminPage} token=${user.token} role=${user.role} />`
@@ -3360,6 +3296,13 @@ function App() {
${page}
</main>
</div>
+ ${musicQueue && html`
+ <${MusicPlayerBar}
+ getConnection=${getMusicConnection}
+ queue=${musicQueue}
+ userPrefs=${userPrefs}
+ onClose=${handleStopMusic} />
+ `}
<//>
`;
}