mirror of
https://github.com/ratspeak/Ratspeak
synced 2026-08-12 18:07:35 -04:00
channels: make hub switching first-class
This commit is contained in:
parent
20207eb9e6
commit
28a3c01a80
6 changed files with 749 additions and 23 deletions
|
|
@ -129,6 +129,9 @@ fn channels_keep_hubs_live_only_and_wire_bounded_local_history_across_the_produc
|
|||
.expect("channels notification route test");
|
||||
let share_test = read_source(root.join("dashboard/scripts/test_channels_share.js"))
|
||||
.expect("channels share test");
|
||||
let hub_switcher_test =
|
||||
read_source(root.join("dashboard/scripts/test_channels_hub_switcher.js"))
|
||||
.expect("channels hub switcher test");
|
||||
let tauri_events =
|
||||
read_source(root.join("dashboard/static/js/tauri_events.js")).expect("tauri event bridge");
|
||||
let tauri_lib = read_source(root.join("src-tauri/src/lib.rs")).expect("tauri lib");
|
||||
|
|
@ -142,6 +145,8 @@ fn channels_keep_hubs_live_only_and_wire_bounded_local_history_across_the_produc
|
|||
assert!(!index.contains("Group conversations"));
|
||||
assert!(!index.contains("channels-sidebar-subtitle"));
|
||||
assert!(index.contains("Available hubs"));
|
||||
assert!(index.contains("id=\"channel-hub-switcher-btn\""));
|
||||
assert!(index.contains("aria-haspopup=\"dialog\""));
|
||||
assert!(index.contains("id=\"channel-owned-hub\""));
|
||||
assert!(index.contains("id=\"channel-owned-hub-manage\""));
|
||||
assert!(!index.contains("channels-refresh-btn"));
|
||||
|
|
@ -162,6 +167,22 @@ fn channels_keep_hubs_live_only_and_wire_bounded_local_history_across_the_produc
|
|||
assert!(channels_js.contains("function _channelsBuildHubMark"));
|
||||
assert!(channels_js.contains("function _channelsHubDistance"));
|
||||
assert!(channels_js.contains("function channelsConnectToHub"));
|
||||
assert!(channels_js.contains("function _channelsHubSwitcherModel"));
|
||||
assert!(channels_js.contains("function _channelsHubConnectMode"));
|
||||
assert!(channels_js.contains("function channelsOpenHubSwitcher"));
|
||||
assert!(channels_js.contains("hubSwitcher.addEventListener('click', channelsOpenHubSwitcher)"));
|
||||
assert!(channels_js.contains("Ratspeak keeps one live hub at a time"));
|
||||
assert!(channels_js.contains("Saved channels and local history stay on this device"));
|
||||
assert!(channels_js.contains("list.setAttribute('aria-live', 'polite')"));
|
||||
assert!(channels_js.contains("list.setAttribute('aria-busy', 'true')"));
|
||||
assert!(channels_js.contains("titleElement.textContent = 'Switch channel hub'"));
|
||||
assert!(channels_js.contains("switching: connectMode.kind === 'switch'"));
|
||||
assert!(channels_js.contains("'Could not switch channel hubs.'"));
|
||||
assert!(channels_js.contains("openedEpoch === _channelsHistoryEpoch"));
|
||||
assert!(
|
||||
channels_js.contains("openedGeneration === (Number(channelsSnapshot.generation) || 0)")
|
||||
);
|
||||
assert!(channels_js.contains("dismissHubSwitcher()"));
|
||||
assert!(channels_js.contains("channelHubOwnDestinationHash"));
|
||||
assert!(!channels_js.contains("hub.nearby ? 'Nearby' : 'Recent'"));
|
||||
assert!(channels_js.contains("TextEncoder"));
|
||||
|
|
@ -175,6 +196,9 @@ fn channels_keep_hubs_live_only_and_wire_bounded_local_history_across_the_produc
|
|||
assert!(!channels_js.contains("function _channelsInitials"));
|
||||
assert!(channels_css.contains(".channel-hub-row-mark"));
|
||||
assert!(channels_css.contains(".channel-hub-row-distance"));
|
||||
assert!(channels_css.contains(".channel-hub-switcher-btn"));
|
||||
assert!(channels_css.contains(".channel-hub-switcher-list .channel-hub-row.current"));
|
||||
assert!(channels_css.contains(".channel-hub-switch-impact"));
|
||||
assert!(channels_js.contains("function _channelsBuildHubNotice"));
|
||||
assert!(channels_js.contains("function _channelsBuildHubGreeting"));
|
||||
assert!(channels_js.contains("function _channelsGroupPresenceEvents"));
|
||||
|
|
@ -400,6 +424,7 @@ fn channels_keep_hubs_live_only_and_wire_bounded_local_history_across_the_produc
|
|||
assert!(channels_css.contains(".channel-share-qr-shell"));
|
||||
assert!(channels_css.contains(".channel-share-input"));
|
||||
assert!(share_test.contains("channel share tests passed"));
|
||||
assert!(hub_switcher_test.contains("channel hub switcher tests passed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
injects pixel values via evaluateJavascript; desktop falls back to 0. -->
|
||||
<title>Ratspeak - Dashboard</title>
|
||||
<link rel="stylesheet" href="/static/fonts/fonts.css">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.0.28">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.0.29">
|
||||
</head>
|
||||
<body class="checking-setup">
|
||||
|
||||
|
|
@ -563,11 +563,16 @@
|
|||
</div>
|
||||
|
||||
<div class="channel-hub-strip" id="channel-hub-strip" data-phase="offline">
|
||||
<span class="channel-live-beacon" aria-hidden="true"></span>
|
||||
<div class="channel-hub-strip-copy">
|
||||
<strong id="channel-hub-strip-title">Not connected</strong>
|
||||
<span id="channel-hub-strip-meta">Choose a hub to begin</span>
|
||||
</div>
|
||||
<button class="channel-hub-switcher-btn" id="channel-hub-switcher-btn" type="button" aria-haspopup="dialog" aria-label="Choose a channel hub">
|
||||
<span class="channel-live-beacon" aria-hidden="true"></span>
|
||||
<span class="channel-hub-strip-copy">
|
||||
<strong id="channel-hub-strip-title">Not connected</strong>
|
||||
<span id="channel-hub-strip-meta">Choose a hub to begin</span>
|
||||
</span>
|
||||
<span class="channel-hub-switcher-chevron" aria-hidden="true">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</span>
|
||||
</button>
|
||||
<button class="nr-btn nr-btn-ghost channels-icon-btn channel-hub-menu-btn" id="channel-hub-menu-btn" type="button" title="Hub options" aria-label="Channel hub options" hidden>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg>
|
||||
</button>
|
||||
|
|
@ -1848,7 +1853,7 @@
|
|||
<script src="/static/js/vendor/jsQR.js"></script>
|
||||
<script src="/static/js/contact_card.js"></script>
|
||||
<script src="/static/js/lxmf.js"></script>
|
||||
<script src="/static/js/channels.js?v=1.0.33"></script>
|
||||
<script src="/static/js/channels.js?v=1.0.34"></script>
|
||||
<script src="/static/js/channel_hub.js?v=1.0.29"></script>
|
||||
<script src="/static/js/propagation.js"></script>
|
||||
<script src="/static/js/settings.js"></script>
|
||||
|
|
|
|||
|
|
@ -107,7 +107,8 @@ assert.strictEqual(
|
|||
);
|
||||
assert.strictEqual(
|
||||
presentationContext.meta({ destination_hash: 'abcd', saved: true, nearby: true }),
|
||||
'short:abcd'
|
||||
'Saved · short:abcd',
|
||||
'saved identity must remain visible even while a hub is also nearby'
|
||||
);
|
||||
|
||||
assert(indexSource.indexOf('Available hubs') !== -1,
|
||||
|
|
|
|||
194
dashboard/scripts/test_channels_hub_switcher.js
Normal file
194
dashboard/scripts/test_channels_hub_switcher.js
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
#!/usr/bin/env node
|
||||
// Deterministic model and source coverage for the one-live-hub switcher.
|
||||
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var vm = require('vm');
|
||||
|
||||
var root = path.join(__dirname, '..');
|
||||
var channelsSource = fs.readFileSync(
|
||||
path.join(root, 'static', 'js', 'channels.js'),
|
||||
'utf8'
|
||||
);
|
||||
var indexSource = fs.readFileSync(path.join(root, 'index.html'), 'utf8');
|
||||
var cssSource = fs.readFileSync(
|
||||
path.join(root, 'static', 'css', '09-channels.css'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
function sourceRange(startName, endName) {
|
||||
var start = channelsSource.indexOf('function ' + startName);
|
||||
var end = channelsSource.indexOf('\nfunction ' + endName, start);
|
||||
assert(start !== -1 && end !== -1, startName + ' source range must exist');
|
||||
return channelsSource.slice(start, end);
|
||||
}
|
||||
|
||||
var modelSource = sourceRange('_channelsMergedHubs', '_channelsSetText');
|
||||
var context = {
|
||||
Object: Object,
|
||||
String: String,
|
||||
channelsSavedHubs: [],
|
||||
channelsDiscoveredHubs: [],
|
||||
channelsSnapshot: {},
|
||||
channelHubOwnDestinationHash: function() { return ''; }
|
||||
};
|
||||
context._channelsIsConnected = function() {
|
||||
return context.channelsSnapshot.phase === 'active' ||
|
||||
context.channelsSnapshot.phase === 'stale';
|
||||
};
|
||||
vm.runInNewContext(modelSource, context, {
|
||||
filename: 'channels-hub-switcher-model.js'
|
||||
});
|
||||
|
||||
var HUB_A = '00112233445566778899aabbccddeeff';
|
||||
var HUB_B = '11112222333344445555666677778888';
|
||||
var HUB_C = '22223333444455556666777788889999';
|
||||
var HUB_D = '3333444455556666777788889999aaaa';
|
||||
|
||||
context.channelsSavedHubs = [
|
||||
{
|
||||
destination_hash: HUB_A,
|
||||
label: 'Home relay',
|
||||
nickname: 'rat',
|
||||
last_connected: 30
|
||||
},
|
||||
{
|
||||
destination_hash: HUB_B,
|
||||
label: 'Field relay',
|
||||
nickname: 'rat',
|
||||
last_connected: 20
|
||||
},
|
||||
{
|
||||
destination_hash: HUB_C,
|
||||
label: 'Archive relay',
|
||||
nickname: 'rat',
|
||||
last_connected: 10
|
||||
}
|
||||
];
|
||||
context.channelsDiscoveredHubs = [
|
||||
{
|
||||
destination_hash: HUB_A,
|
||||
announced_name: 'Home',
|
||||
hops: 0,
|
||||
last_seen: 100
|
||||
},
|
||||
{
|
||||
destination_hash: HUB_B,
|
||||
announced_name: 'Field',
|
||||
hops: 1,
|
||||
last_seen: 90
|
||||
},
|
||||
{
|
||||
destination_hash: HUB_D,
|
||||
announced_name: 'Ridge',
|
||||
hops: 2,
|
||||
last_seen: 80
|
||||
}
|
||||
];
|
||||
context.channelsSnapshot = {
|
||||
phase: 'active',
|
||||
selected_hub_destination: HUB_A,
|
||||
hub: {
|
||||
destination_hash: HUB_A,
|
||||
name: 'Home',
|
||||
hops: 0
|
||||
}
|
||||
};
|
||||
|
||||
var model = context._channelsHubSwitcherModel();
|
||||
assert.strictEqual(model.current.destination_hash, HUB_A);
|
||||
assert.strictEqual(model.current.saved, true);
|
||||
assert.strictEqual(model.current.nearby, true);
|
||||
assert.strictEqual(
|
||||
Array.prototype.map.call(model.nearby, function(hub) {
|
||||
return hub.destination_hash;
|
||||
}).join(','),
|
||||
HUB_B + ',' + HUB_D,
|
||||
'recently heard choices must preserve hop-first ordering'
|
||||
);
|
||||
assert.strictEqual(model.saved.length, 1);
|
||||
assert.strictEqual(model.saved[0].destination_hash, HUB_C);
|
||||
assert(
|
||||
!Array.prototype.some.call(model.nearby, function(hub) {
|
||||
return hub.destination_hash === HUB_A;
|
||||
}),
|
||||
'the selected hub must be pinned once, not repeated as another choice'
|
||||
);
|
||||
|
||||
context.channelsSavedHubs = [];
|
||||
context.channelsDiscoveredHubs = [];
|
||||
context.channelsSnapshot = {
|
||||
phase: 'connecting',
|
||||
selected_hub_destination: HUB_D,
|
||||
hub: {
|
||||
destination_hash: HUB_D,
|
||||
announced_name: 'Uncached target'
|
||||
}
|
||||
};
|
||||
model = context._channelsHubSwitcherModel();
|
||||
assert.strictEqual(model.current.destination_hash, HUB_D,
|
||||
'an unsaved in-flight target must remain visible as the current selection');
|
||||
assert.strictEqual(model.current.announced_name, 'Uncached target');
|
||||
|
||||
function mode(phase, target, selected) {
|
||||
context.channelsSnapshot = {
|
||||
phase: phase,
|
||||
selected_hub_destination: selected || null,
|
||||
hub: selected ? { destination_hash: selected } : null
|
||||
};
|
||||
return context._channelsHubConnectMode(target).kind;
|
||||
}
|
||||
|
||||
assert.strictEqual(mode('active', HUB_A, HUB_A), 'current');
|
||||
assert.strictEqual(mode('stale', HUB_B, HUB_A), 'switch');
|
||||
assert.strictEqual(mode('reconnecting', HUB_A, HUB_A), 'recovering');
|
||||
assert.strictEqual(mode('reconnecting', HUB_B, HUB_A), 'switch',
|
||||
'a person may deliberately replace a bounded recovery attempt');
|
||||
assert.strictEqual(mode('resolving', HUB_B, HUB_A), 'pending');
|
||||
assert.strictEqual(mode('connecting', HUB_B, HUB_A), 'pending');
|
||||
assert.strictEqual(mode('awaiting_welcome', HUB_B, HUB_A), 'pending');
|
||||
assert.strictEqual(mode('error', HUB_A, HUB_A), 'connect',
|
||||
'an ended attempt remains retryable');
|
||||
assert.strictEqual(mode('offline', HUB_B, null), 'connect');
|
||||
|
||||
var stripPosition = indexSource.indexOf('id="channel-hub-switcher-btn"');
|
||||
var listPosition = indexSource.indexOf('id="channels-list"');
|
||||
assert(stripPosition !== -1 && stripPosition < listPosition,
|
||||
'the hub selector must remain visibly above the selected hub channel list');
|
||||
assert(indexSource.indexOf('aria-haspopup="dialog"') !== -1);
|
||||
assert(channelsSource.indexOf('function channelsOpenHubSwitcher()') !== -1);
|
||||
assert(channelsSource.indexOf("hubSwitcher.addEventListener('click', channelsOpenHubSwitcher)") !== -1);
|
||||
assert(channelsSource.indexOf('Ratspeak keeps one live hub at a time.') !== -1);
|
||||
assert(channelsSource.indexOf('Saved channels and local history stay on this device.') !== -1);
|
||||
assert(channelsSource.indexOf("list.setAttribute('aria-live', 'polite')") !== -1);
|
||||
assert(channelsSource.indexOf("list.setAttribute('aria-busy', 'true')") !== -1);
|
||||
assert(channelsSource.indexOf("titleElement.textContent = 'Switch channel hub'") !== -1);
|
||||
assert(channelsSource.indexOf("'Switch hub'") !== -1);
|
||||
assert(channelsSource.indexOf("'Switching channel hub\\u2026'") !== -1);
|
||||
assert(channelsSource.indexOf("switching: connectMode.kind === 'switch'") !== -1);
|
||||
assert(channelsSource.indexOf("'Could not switch channel hubs.'") !== -1);
|
||||
assert(channelsSource.indexOf("case 'reconnecting': return { label: 'Reconnecting'") !== -1);
|
||||
assert(channelsSource.indexOf('openedEpoch === _channelsHistoryEpoch') !== -1);
|
||||
assert(channelsSource.indexOf('openedGeneration === (Number(channelsSnapshot.generation) || 0)') !== -1);
|
||||
assert(channelsSource.indexOf('dismissHubSwitcher()') !== -1,
|
||||
'an identity change must close the old identity switcher');
|
||||
assert(channelsSource.indexOf("RS.invoke('connect_channel_hub'") !== -1);
|
||||
assert(channelsSource.indexOf('CHANNELS_CONNECTION_BUDGET') === -1,
|
||||
'the frontend must not invent or raise the runtime connection budget');
|
||||
|
||||
var switcherSource = sourceRange('channelsOpenHubSwitcher', 'channelsOpenConnectSheet');
|
||||
assert(switcherSource.indexOf('localStorage') === -1);
|
||||
assert(switcherSource.indexOf("RS.invoke('connect_channel_hub'") === -1,
|
||||
'choosing a hub must open explicit review instead of connecting from the switcher');
|
||||
assert(switcherSource.indexOf('channelsRefreshAvailableHubs()') !== -1);
|
||||
assert(switcherSource.indexOf('Scan') === -1,
|
||||
'the recent announce cache must not be presented as an active network scan');
|
||||
|
||||
assert(cssSource.indexOf('.channel-hub-switcher-btn') !== -1);
|
||||
assert(cssSource.indexOf('.channel-hub-switcher-list .channel-hub-row.current') !== -1);
|
||||
assert(cssSource.indexOf('.channel-hub-switch-impact') !== -1);
|
||||
|
||||
console.log('channel hub switcher tests passed');
|
||||
|
|
@ -102,6 +102,7 @@
|
|||
.channels-icon-btn:focus-visible,
|
||||
.channels-text-btn:focus-visible,
|
||||
.message-mode-option:focus-visible,
|
||||
.channel-hub-switcher-btn:focus-visible,
|
||||
.channel-hub-row:focus-visible,
|
||||
.channel-room-row:focus-visible,
|
||||
.channels-list-section-action:focus-visible,
|
||||
|
|
@ -113,15 +114,39 @@
|
|||
.channel-hub-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-5);
|
||||
min-height: 54px;
|
||||
margin: 0 var(--space-5) var(--space-5);
|
||||
padding: var(--space-5) var(--space-6);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-surface-soft);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--surface-panel-muted);
|
||||
}
|
||||
|
||||
.channel-hub-switcher-btn {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 52px;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
gap: var(--space-5);
|
||||
padding: var(--space-5) var(--space-4) var(--space-5) var(--space-6);
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-fast);
|
||||
}
|
||||
|
||||
.channel-hub-switcher-btn:hover {
|
||||
background: var(--surface-row-hover);
|
||||
}
|
||||
|
||||
.channel-hub-switcher-btn:focus-visible {
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
.channel-hub-strip[data-phase="active"],
|
||||
.channel-hub-strip[data-phase="stale"] {
|
||||
border-color: var(--status-online-border);
|
||||
|
|
@ -199,10 +224,23 @@
|
|||
width: 30px;
|
||||
height: 30px;
|
||||
flex-basis: 30px;
|
||||
margin-right: var(--space-3);
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.channel-hub-switcher-chevron {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
color: var(--text-muted);
|
||||
transition: transform var(--transition-fast), color var(--transition-fast);
|
||||
}
|
||||
|
||||
.channel-hub-switcher-btn:hover .channel-hub-switcher-chevron {
|
||||
color: var(--text-secondary);
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.channel-owned-hub {
|
||||
padding: 0 var(--space-4) var(--space-4);
|
||||
border-bottom: 1px solid var(--border-row);
|
||||
|
|
@ -601,6 +639,13 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.channel-hub-row-trailing {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: flex-end;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.channel-row-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -623,6 +668,11 @@
|
|||
color: var(--status-error);
|
||||
}
|
||||
|
||||
.channel-row-status.recovering {
|
||||
background: var(--status-warning-bg);
|
||||
color: var(--status-warning);
|
||||
}
|
||||
|
||||
.channel-room-row.has-unread .channel-room-row-title {
|
||||
font-weight: var(--type-weight-semibold);
|
||||
}
|
||||
|
|
@ -1750,6 +1800,20 @@
|
|||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.channel-hub-switch-impact {
|
||||
padding: var(--space-4) var(--space-5);
|
||||
border-left: 3px solid var(--status-warning);
|
||||
border-radius: 0 var(--radius-md) var(--radius-md) 0;
|
||||
background: var(--surface-panel-muted);
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.channel-hub-switch-impact[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.channel-sheet-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -1786,6 +1850,42 @@
|
|||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.channel-hub-switcher-sheet .bottom-sheet-body {
|
||||
gap: var(--space-5);
|
||||
}
|
||||
|
||||
.channel-hub-switcher-list {
|
||||
display: flex;
|
||||
min-height: 96px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.channel-hub-switcher-list .channels-list-section {
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
.channel-hub-switcher-list .channel-hub-row {
|
||||
min-height: 58px;
|
||||
margin-bottom: var(--space-2);
|
||||
border-color: var(--border-surface-soft);
|
||||
background: var(--surface-panel-muted);
|
||||
}
|
||||
|
||||
.channel-hub-switcher-list .channel-hub-row:hover:not(:disabled) {
|
||||
border-color: var(--border-surface-strong);
|
||||
background: var(--surface-row-hover);
|
||||
}
|
||||
|
||||
.channel-hub-switcher-list .channel-hub-row.current {
|
||||
border-color: var(--accent-border);
|
||||
background: var(--accent-bg);
|
||||
}
|
||||
|
||||
.channel-hub-switcher-list .channel-hub-row:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: var(--opacity-disabled);
|
||||
}
|
||||
|
||||
.channel-sheet-hub {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ var _channelsHistoryEpoch = 0;
|
|||
var _channelsRoomIndexRequestSeq = 0;
|
||||
var _channelsUnreadRequestSeq = 0;
|
||||
var _channelsRenderedRoomKey = '';
|
||||
var _channelsHubSwitcherDismiss = null;
|
||||
var CHANNEL_PRESENCE_GROUP_WINDOW_MS = 5 * 60 * 1000;
|
||||
// Brief leave/rejoin churn is one continuous presence when nothing happens between it.
|
||||
var CHANNEL_PRESENCE_REJOIN_WINDOW_MS = 15 * 1000;
|
||||
|
|
@ -306,7 +307,13 @@ function channelsConnectToHub(hub, options) {
|
|||
channelsHistorySelection = null;
|
||||
channelsApplySnapshot(snapshot);
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Connecting to channel hub\u2026', 'toast-blue', 2600);
|
||||
showToast(
|
||||
options.switching
|
||||
? 'Switching channel hub\u2026'
|
||||
: 'Connecting to channel hub\u2026',
|
||||
'toast-blue',
|
||||
2600
|
||||
);
|
||||
}
|
||||
return snapshot;
|
||||
});
|
||||
|
|
@ -558,6 +565,101 @@ function _channelsMergedHubs() {
|
|||
});
|
||||
}
|
||||
|
||||
function _channelsSelectedHubDestination() {
|
||||
return String(
|
||||
channelsSnapshot.selected_hub_destination ||
|
||||
(channelsSnapshot.hub && channelsSnapshot.hub.destination_hash) ||
|
||||
''
|
||||
).trim().toLowerCase();
|
||||
}
|
||||
|
||||
// One stable navigation model for the measured one-live-hub product budget.
|
||||
// The selected hub is pinned separately; recently heard hubs and saved-only
|
||||
// hubs remain visible choices without implying background presence.
|
||||
function _channelsHubSwitcherModel() {
|
||||
var selected = _channelsSelectedHubDestination();
|
||||
var merged = _channelsMergedHubs();
|
||||
var current = null;
|
||||
var nearby = [];
|
||||
var saved = [];
|
||||
|
||||
merged.forEach(function(hub) {
|
||||
if (selected && hub.destination_hash === selected) {
|
||||
current = Object.assign({}, hub);
|
||||
} else if (hub.nearby) {
|
||||
nearby.push(hub);
|
||||
} else if (hub.saved) {
|
||||
saved.push(hub);
|
||||
}
|
||||
});
|
||||
|
||||
if (selected) {
|
||||
var observed = channelsSnapshot.hub &&
|
||||
String(channelsSnapshot.hub.destination_hash || '').toLowerCase() === selected
|
||||
? channelsSnapshot.hub
|
||||
: null;
|
||||
current = Object.assign(
|
||||
{ destination_hash: selected, saved: false, nearby: false },
|
||||
current || {},
|
||||
observed || {}
|
||||
);
|
||||
current.destination_hash = selected;
|
||||
}
|
||||
|
||||
return {
|
||||
current: current,
|
||||
nearby: nearby,
|
||||
saved: saved
|
||||
};
|
||||
}
|
||||
|
||||
function _channelsConnectCommandBlocked() {
|
||||
return channelsSnapshot.phase === 'resolving' ||
|
||||
channelsSnapshot.phase === 'connecting' ||
|
||||
channelsSnapshot.phase === 'awaiting_welcome';
|
||||
}
|
||||
|
||||
function _channelsHubConnectMode(destinationHash) {
|
||||
var destination = String(destinationHash || '').trim().toLowerCase();
|
||||
var current = _channelsSelectedHubDestination();
|
||||
var same = !!destination && !!current && destination === current;
|
||||
|
||||
if (_channelsConnectCommandBlocked()) {
|
||||
return {
|
||||
kind: 'pending',
|
||||
current_destination: current,
|
||||
same_destination: same
|
||||
};
|
||||
}
|
||||
if (same && _channelsIsConnected()) {
|
||||
return {
|
||||
kind: 'current',
|
||||
current_destination: current,
|
||||
same_destination: true
|
||||
};
|
||||
}
|
||||
if (same && channelsSnapshot.phase === 'reconnecting') {
|
||||
return {
|
||||
kind: 'recovering',
|
||||
current_destination: current,
|
||||
same_destination: true
|
||||
};
|
||||
}
|
||||
if (current && destination && destination !== current &&
|
||||
(_channelsIsConnected() || channelsSnapshot.phase === 'reconnecting')) {
|
||||
return {
|
||||
kind: 'switch',
|
||||
current_destination: current,
|
||||
same_destination: false
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: 'connect',
|
||||
current_destination: current,
|
||||
same_destination: same
|
||||
};
|
||||
}
|
||||
|
||||
function _channelsSetText(id, value) {
|
||||
var el = _channelsEl(id);
|
||||
if (el) el.textContent = value == null ? '' : String(value);
|
||||
|
|
@ -1186,6 +1288,7 @@ function renderChannels() {
|
|||
|
||||
function _channelsRenderHubStrip() {
|
||||
var strip = _channelsEl('channel-hub-strip');
|
||||
var switcher = _channelsEl('channel-hub-switcher-btn');
|
||||
var menu = _channelsEl('channel-hub-menu-btn');
|
||||
if (!strip) return;
|
||||
|
||||
|
|
@ -1211,6 +1314,16 @@ function _channelsRenderHubStrip() {
|
|||
}
|
||||
_channelsSetText('channel-hub-strip-title', title);
|
||||
_channelsSetText('channel-hub-strip-meta', meta);
|
||||
if (switcher) {
|
||||
var currentName = hub ? _channelsHubName(hub) : '';
|
||||
switcher.setAttribute(
|
||||
'aria-label',
|
||||
currentName
|
||||
? 'Choose a channel hub. Current selection: ' + currentName
|
||||
: 'Choose a channel hub'
|
||||
);
|
||||
switcher.title = currentName ? 'Switch channel hub' : 'Choose a channel hub';
|
||||
}
|
||||
}
|
||||
|
||||
function _channelsRenderList() {
|
||||
|
|
@ -1413,7 +1526,7 @@ function _channelsHubDistance(hub) {
|
|||
|
||||
function _channelsHubMeta(hub) {
|
||||
var hash = _channelsShortHash(hub && hub.destination_hash);
|
||||
return hub && hub.saved && !hub.nearby ? 'Saved \u00b7 ' + hash : hash;
|
||||
return hub && hub.saved ? 'Saved \u00b7 ' + hash : hash;
|
||||
}
|
||||
|
||||
function _channelsBuildHubMark(hub) {
|
||||
|
|
@ -1425,11 +1538,14 @@ function _channelsBuildHubMark(hub) {
|
|||
return mark;
|
||||
}
|
||||
|
||||
function _channelsBuildHubRow(hub) {
|
||||
function _channelsBuildHubRow(hub, options) {
|
||||
options = options || {};
|
||||
var row = document.createElement('button');
|
||||
row.type = 'button';
|
||||
row.className = 'channel-hub-row';
|
||||
row.className = 'channel-hub-row' + (options.current ? ' current' : '');
|
||||
row.dataset.destinationHash = hub.destination_hash;
|
||||
if (options.current) row.setAttribute('aria-current', 'true');
|
||||
row.disabled = !!options.disabled;
|
||||
|
||||
row.appendChild(_channelsBuildHubMark(hub));
|
||||
|
||||
|
|
@ -1445,11 +1561,24 @@ function _channelsBuildHubRow(hub) {
|
|||
copy.appendChild(meta);
|
||||
row.appendChild(copy);
|
||||
|
||||
var trailing = document.createElement('span');
|
||||
trailing.className = 'channel-hub-row-trailing';
|
||||
var distance = document.createElement('span');
|
||||
distance.className = 'channel-hub-row-distance';
|
||||
distance.textContent = _channelsHubDistance(hub);
|
||||
if (distance.textContent) row.appendChild(distance);
|
||||
row.addEventListener('click', function() { channelsOpenConnectSheet(hub); });
|
||||
if (distance.textContent) trailing.appendChild(distance);
|
||||
if (options.status) {
|
||||
var status = document.createElement('span');
|
||||
status.className = 'channel-row-status' +
|
||||
(options.statusTone ? ' ' + options.statusTone : '');
|
||||
status.textContent = options.status;
|
||||
trailing.appendChild(status);
|
||||
}
|
||||
if (trailing.childNodes.length) row.appendChild(trailing);
|
||||
row.addEventListener('click', function() {
|
||||
if (typeof options.onSelect === 'function') options.onSelect(hub);
|
||||
else channelsOpenConnectSheet(hub);
|
||||
});
|
||||
return row;
|
||||
}
|
||||
|
||||
|
|
@ -3140,6 +3269,203 @@ function channelsOpenSharedChannel(payload) {
|
|||
}
|
||||
window.channelsOpenSharedChannel = channelsOpenSharedChannel;
|
||||
|
||||
function _channelsHubCurrentStatus() {
|
||||
switch (channelsSnapshot.phase) {
|
||||
case 'active': return { label: 'Live now', tone: 'joined' };
|
||||
case 'stale': return { label: 'Recovering', tone: 'recovering' };
|
||||
case 'reconnecting': return { label: 'Reconnecting', tone: 'recovering' };
|
||||
case 'error': return { label: 'Needs attention', tone: 'error' };
|
||||
default: return { label: _channelsPhaseLabel(channelsSnapshot.phase), tone: '' };
|
||||
}
|
||||
}
|
||||
|
||||
function channelsOpenHubSwitcher() {
|
||||
if (typeof _rsBuildSheet !== 'function') return;
|
||||
if (typeof _channelsHubSwitcherDismiss === 'function') {
|
||||
_channelsHubSwitcherDismiss();
|
||||
}
|
||||
var openedEpoch = _channelsHistoryEpoch;
|
||||
var openedGeneration = Number(channelsSnapshot.generation) || 0;
|
||||
var built = _rsBuildSheet({ title: 'Channel hubs' }, function() {
|
||||
if (_channelsHubSwitcherDismiss === built.dismiss) {
|
||||
_channelsHubSwitcherDismiss = null;
|
||||
}
|
||||
});
|
||||
_channelsHubSwitcherDismiss = built.dismiss;
|
||||
built.sheet.classList.add('channel-hub-switcher-sheet');
|
||||
|
||||
var copy = document.createElement('p');
|
||||
copy.className = 'channel-sheet-copy';
|
||||
copy.textContent = 'Ratspeak keeps one live hub at a time. Switching ends the current live rooms; saved channels and local history stay on this device.';
|
||||
built.body.appendChild(copy);
|
||||
|
||||
var list = document.createElement('div');
|
||||
list.className = 'channel-hub-switcher-list';
|
||||
list.setAttribute('aria-live', 'polite');
|
||||
built.body.appendChild(list);
|
||||
var refreshInFlight = false;
|
||||
|
||||
function contextIsCurrent() {
|
||||
return openedEpoch === _channelsHistoryEpoch &&
|
||||
openedGeneration === (Number(channelsSnapshot.generation) || 0);
|
||||
}
|
||||
|
||||
function retireStaleSwitcher() {
|
||||
built.dismiss();
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Channels changed. Choose a hub again.', 'toast-orange', 2800);
|
||||
}
|
||||
}
|
||||
|
||||
function chooseHub(hub, current) {
|
||||
if (!contextIsCurrent()) {
|
||||
retireStaleSwitcher();
|
||||
return;
|
||||
}
|
||||
built.dismiss();
|
||||
setTimeout(function() {
|
||||
if (!contextIsCurrent()) {
|
||||
retireStaleSwitcher();
|
||||
return;
|
||||
}
|
||||
if (current && channelsSnapshot.hub) channelsOpenHubOptions();
|
||||
else channelsOpenConnectSheet(hub);
|
||||
}, 220);
|
||||
}
|
||||
|
||||
function appendHubSection(label, hubs, options) {
|
||||
options = options || {};
|
||||
if (!hubs.length && !options.always) return;
|
||||
var section = _channelsListSection(label, {
|
||||
className: 'channel-hub-switcher-section',
|
||||
actionText: options.actionText,
|
||||
actionDisabled: options.actionDisabled,
|
||||
action: options.action
|
||||
});
|
||||
if (options.refreshAction) {
|
||||
var sectionAction = section.querySelector('.channels-list-section-action');
|
||||
if (sectionAction) sectionAction.dataset.channelHubRefresh = 'true';
|
||||
}
|
||||
list.appendChild(section);
|
||||
hubs.forEach(function(hub) {
|
||||
list.appendChild(_channelsBuildHubRow(hub, {
|
||||
current: !!options.current,
|
||||
disabled: !!options.disabled,
|
||||
status: options.status || '',
|
||||
statusTone: options.statusTone || '',
|
||||
onSelect: function(selected) {
|
||||
chooseHub(selected, !!options.current);
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function refreshNearby() {
|
||||
if (refreshInFlight) return;
|
||||
if (!contextIsCurrent()) {
|
||||
retireStaleSwitcher();
|
||||
return;
|
||||
}
|
||||
refreshInFlight = true;
|
||||
list.setAttribute('aria-busy', 'true');
|
||||
var refreshButton = list.querySelector('[data-channel-hub-refresh]');
|
||||
if (refreshButton) {
|
||||
refreshButton.disabled = true;
|
||||
refreshButton.textContent = 'Checking\u2026';
|
||||
}
|
||||
channelsRefreshAvailableHubs().then(function() {
|
||||
refreshInFlight = false;
|
||||
list.removeAttribute('aria-busy');
|
||||
if (built.sheet.isConnected && contextIsCurrent()) {
|
||||
renderList();
|
||||
var nextRefresh = list.querySelector('[data-channel-hub-refresh]');
|
||||
if (nextRefresh) nextRefresh.focus();
|
||||
} else if (built.sheet.isConnected) {
|
||||
retireStaleSwitcher();
|
||||
}
|
||||
}, function() {
|
||||
refreshInFlight = false;
|
||||
list.removeAttribute('aria-busy');
|
||||
if (built.sheet.isConnected && contextIsCurrent()) {
|
||||
renderList();
|
||||
var nextRefresh = list.querySelector('[data-channel-hub-refresh]');
|
||||
if (nextRefresh) nextRefresh.focus();
|
||||
} else if (built.sheet.isConnected) {
|
||||
retireStaleSwitcher();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
list.textContent = '';
|
||||
var model = _channelsHubSwitcherModel();
|
||||
var blocked = _channelsConnectCommandBlocked();
|
||||
if (model.current) {
|
||||
var currentStatus = _channelsHubCurrentStatus();
|
||||
appendHubSection('Current selection', [model.current], {
|
||||
current: true,
|
||||
status: currentStatus.label,
|
||||
statusTone: currentStatus.tone
|
||||
});
|
||||
}
|
||||
appendHubSection('Recently heard', model.nearby, {
|
||||
always: true,
|
||||
actionText: refreshInFlight ? 'Checking\u2026' : 'Refresh',
|
||||
actionDisabled: refreshInFlight,
|
||||
action: refreshNearby,
|
||||
refreshAction: true,
|
||||
disabled: blocked
|
||||
});
|
||||
if (!model.nearby.length) {
|
||||
list.appendChild(_channelsDirectoryStatus(
|
||||
refreshInFlight ? 'Checking recent hub announcements\u2026' : 'No other hubs heard recently',
|
||||
'Saved hubs remain available below'
|
||||
));
|
||||
}
|
||||
appendHubSection('Saved on this device', model.saved, {
|
||||
disabled: blocked
|
||||
});
|
||||
if (!model.current && !model.nearby.length && !model.saved.length) {
|
||||
list.appendChild(_channelsDirectoryStatus(
|
||||
'No channel hubs yet',
|
||||
'Add a trusted destination address to begin'
|
||||
));
|
||||
}
|
||||
if (blocked && (model.nearby.length || model.saved.length)) {
|
||||
list.appendChild(_channelsDirectoryStatus(
|
||||
'Connection in progress',
|
||||
'Open the current selection to cancel before choosing another hub',
|
||||
'warning'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
var add = document.createElement('button');
|
||||
add.type = 'button';
|
||||
add.className = 'nr-btn nr-btn-secondary';
|
||||
add.textContent = 'Add by address';
|
||||
add.addEventListener('click', function() {
|
||||
built.dismiss();
|
||||
setTimeout(function() { channelsOpenConnectSheet(); }, 220);
|
||||
});
|
||||
var done = document.createElement('button');
|
||||
done.type = 'button';
|
||||
done.className = 'nr-btn nr-btn-primary';
|
||||
done.textContent = 'Done';
|
||||
done.addEventListener('click', built.dismiss);
|
||||
built.footer.appendChild(add);
|
||||
built.footer.appendChild(done);
|
||||
|
||||
renderList();
|
||||
_channelsPresentSheet(built, modelCurrentFocus());
|
||||
|
||||
function modelCurrentFocus() {
|
||||
return list.querySelector('.channel-hub-row.current') ||
|
||||
list.querySelector('.channel-hub-row:not(:disabled)') ||
|
||||
add;
|
||||
}
|
||||
}
|
||||
|
||||
function channelsOpenConnectSheet(prefill) {
|
||||
if (typeof _rsBuildSheet !== 'function') return;
|
||||
prefill = prefill || {};
|
||||
|
|
@ -3152,6 +3478,7 @@ function channelsOpenConnectSheet(prefill) {
|
|||
var sharedRoom = prefill.shared_room || '';
|
||||
var defaultNick = prefill.nickname || (_channelsSavedHub(selectedHash) || {}).nickname || channelsSnapshot.nickname || _channelsDefaultNickname();
|
||||
var built = _rsBuildSheet({ title: 'Connect to Channels' }, function() {});
|
||||
var titleElement = built.sheet.querySelector('.bottom-sheet-title');
|
||||
|
||||
var copy = document.createElement('p');
|
||||
copy.className = 'channel-sheet-copy';
|
||||
|
|
@ -3175,6 +3502,11 @@ function channelsOpenConnectSheet(prefill) {
|
|||
trust.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg><span>Choose a hub you trust. The Link is encrypted in transit, but the hub relays and can read channel messages.</span>';
|
||||
built.body.appendChild(trust);
|
||||
|
||||
var switchImpact = document.createElement('div');
|
||||
switchImpact.className = 'channel-hub-switch-impact';
|
||||
switchImpact.hidden = true;
|
||||
built.body.appendChild(switchImpact);
|
||||
|
||||
var destinationInput = document.createElement('input');
|
||||
destinationInput.type = 'text';
|
||||
destinationInput.className = 'nr-input-sm mono';
|
||||
|
|
@ -3188,6 +3520,7 @@ function channelsOpenConnectSheet(prefill) {
|
|||
var entered = destinationInput.value.trim().toLowerCase();
|
||||
if (entered !== selectedHash) selectedLabel = '';
|
||||
if (entered !== sharedDestination) sharedRoom = '';
|
||||
updateConnectionChoice();
|
||||
});
|
||||
|
||||
if (hubs.length) {
|
||||
|
|
@ -3226,6 +3559,7 @@ function channelsOpenConnectSheet(prefill) {
|
|||
available.querySelectorAll('.channel-sheet-hub').forEach(function(el) { el.classList.remove('selected'); });
|
||||
row.classList.add('selected');
|
||||
error.textContent = '';
|
||||
updateConnectionChoice();
|
||||
});
|
||||
available.appendChild(row);
|
||||
});
|
||||
|
|
@ -3254,10 +3588,65 @@ function channelsOpenConnectSheet(prefill) {
|
|||
var connect = document.createElement('button');
|
||||
connect.type = 'button';
|
||||
connect.className = 'nr-btn nr-btn-primary';
|
||||
connect.textContent = sharedRoom ? 'Connect and review' : 'Connect';
|
||||
var connectBusy = false;
|
||||
|
||||
function updateConnectionChoice() {
|
||||
var destination = destinationInput.value.trim().toLowerCase();
|
||||
var mode = _channelsHubConnectMode(destination);
|
||||
var currentHub = mode.current_destination
|
||||
? _channelsHubByDestination(mode.current_destination)
|
||||
: null;
|
||||
var currentName = currentHub
|
||||
? _channelsTimelineHubName(currentHub)
|
||||
: 'the current hub';
|
||||
switchImpact.hidden = true;
|
||||
|
||||
if (mode.kind === 'switch') {
|
||||
if (titleElement) titleElement.textContent = 'Switch channel hub';
|
||||
copy.textContent = sharedRoom
|
||||
? 'This share points to ' + sharedRoom + '. Review and authenticate the new hub first; Ratspeak will then open a separate join review.'
|
||||
: 'Review the next hub and nickname before replacing the current live session.';
|
||||
switchImpact.hidden = false;
|
||||
switchImpact.textContent = channelsSnapshot.phase === 'reconnecting'
|
||||
? 'Switching stops recovery for ' + currentName + '. Saved channels and local history stay on this device.'
|
||||
: 'Switching ends your live rooms on ' + currentName + '. Saved channels and local history stay on this device.';
|
||||
connect.textContent = connectBusy
|
||||
? 'Switching\u2026'
|
||||
: (sharedRoom ? 'Switch and review' : 'Switch hub');
|
||||
connect.disabled = connectBusy;
|
||||
} else if (mode.kind === 'current') {
|
||||
if (titleElement) titleElement.textContent = 'Current channel hub';
|
||||
copy.textContent = 'You are already connected to this hub. Choose another destination to switch, or use Hub options to manage this session.';
|
||||
connect.textContent = 'Connected';
|
||||
connect.disabled = true;
|
||||
} else if (mode.kind === 'recovering') {
|
||||
if (titleElement) titleElement.textContent = 'Hub recovery in progress';
|
||||
copy.textContent = 'Ratspeak is recovering this hub connection. Choose another destination to stop recovery and switch.';
|
||||
connect.textContent = 'Recovering\u2026';
|
||||
connect.disabled = true;
|
||||
} else if (mode.kind === 'pending') {
|
||||
if (titleElement) titleElement.textContent = 'Connection in progress';
|
||||
copy.textContent = 'Finish or cancel the current connection before choosing another hub.';
|
||||
switchImpact.hidden = false;
|
||||
switchImpact.textContent = 'Open the current hub from the switcher to cancel this attempt.';
|
||||
connect.textContent = 'Connection in progress';
|
||||
connect.disabled = true;
|
||||
} else {
|
||||
if (titleElement) titleElement.textContent = 'Connect to Channels';
|
||||
copy.textContent = sharedRoom
|
||||
? 'This share points to ' + sharedRoom + '. Review and authenticate the hub first; Ratspeak will then open a separate join review.'
|
||||
: 'Choose a recently heard hub or enter its destination. Ratspeak will authenticate the connection.';
|
||||
connect.textContent = connectBusy
|
||||
? 'Connecting\u2026'
|
||||
: (sharedRoom ? 'Connect and review' : 'Connect');
|
||||
connect.disabled = connectBusy;
|
||||
}
|
||||
}
|
||||
|
||||
connect.addEventListener('click', function() {
|
||||
var destination = destinationInput.value.trim().toLowerCase();
|
||||
var nickname = nicknameInput.value.trim();
|
||||
var connectMode = _channelsHubConnectMode(destination);
|
||||
if (!/^[0-9a-f]{32}$/.test(destination)) {
|
||||
error.textContent = 'Enter a 32-character hexadecimal destination hash.';
|
||||
destinationInput.focus();
|
||||
|
|
@ -3268,8 +3657,8 @@ function channelsOpenConnectSheet(prefill) {
|
|||
nicknameInput.focus();
|
||||
return;
|
||||
}
|
||||
connect.disabled = true;
|
||||
connect.textContent = 'Connecting\u2026';
|
||||
connectBusy = true;
|
||||
updateConnectionChoice();
|
||||
error.textContent = '';
|
||||
var pendingShare = sharedRoom && destination === sharedDestination
|
||||
? {
|
||||
|
|
@ -3284,20 +3673,25 @@ function channelsOpenConnectSheet(prefill) {
|
|||
announced_name: selectedLabel,
|
||||
nickname: nickname
|
||||
}, {
|
||||
preserve_pending_share: true
|
||||
preserve_pending_share: true,
|
||||
switching: connectMode.kind === 'switch'
|
||||
}).then(function() {
|
||||
built.dismiss();
|
||||
}).catch(function(err) {
|
||||
if (channelsPendingShareJoin === pendingShare) {
|
||||
channelsPendingShareJoin = null;
|
||||
}
|
||||
error.textContent = (err && err.message) || 'Could not connect to this hub.';
|
||||
connect.disabled = false;
|
||||
connect.textContent = sharedRoom ? 'Connect and review' : 'Connect';
|
||||
error.textContent = (err && err.message) ||
|
||||
(connectMode.kind === 'switch'
|
||||
? 'Could not switch channel hubs.'
|
||||
: 'Could not connect to this hub.');
|
||||
connectBusy = false;
|
||||
updateConnectionChoice();
|
||||
});
|
||||
});
|
||||
built.footer.appendChild(cancel);
|
||||
built.footer.appendChild(connect);
|
||||
updateConnectionChoice();
|
||||
_channelsPresentSheet(built, selectedHash ? nicknameInput : destinationInput);
|
||||
}
|
||||
|
||||
|
|
@ -3822,6 +4216,8 @@ function _channelsBindUI() {
|
|||
});
|
||||
var join = _channelsEl('channels-join-btn');
|
||||
if (join) join.addEventListener('click', function() { channelsOpenJoinSheet(); });
|
||||
var hubSwitcher = _channelsEl('channel-hub-switcher-btn');
|
||||
if (hubSwitcher) hubSwitcher.addEventListener('click', channelsOpenHubSwitcher);
|
||||
var hubMenu = _channelsEl('channel-hub-menu-btn');
|
||||
if (hubMenu) hubMenu.addEventListener('click', channelsOpenHubOptions);
|
||||
var roomMenu = _channelsEl('channel-room-menu-btn');
|
||||
|
|
@ -3903,6 +4299,11 @@ RS.listen('announce_received', function() {
|
|||
});
|
||||
|
||||
RS.listen('lxmf_identity', function() {
|
||||
if (typeof _channelsHubSwitcherDismiss === 'function') {
|
||||
var dismissHubSwitcher = _channelsHubSwitcherDismiss;
|
||||
_channelsHubSwitcherDismiss = null;
|
||||
dismissHubSwitcher();
|
||||
}
|
||||
channelsSavedHubs = [];
|
||||
channelsSavedRooms = [];
|
||||
channelsRoomIndex = [];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue