diff --git a/app/components/channel-nav.hbs b/app/components/channel-nav.hbs
index 4f1f4848..4048fb66 100644
--- a/app/components/channel-nav.hbs
+++ b/app/components/channel-nav.hbs
@@ -18,7 +18,7 @@
{{/if}}
{{else}}
{{/if}}
kosmos-dev@server). Multi-hash
+ // channels preserve all '#' characters (e.g. ##kosmos-dev ->
+ // ##kosmos-dev@server, URL-encoded as %23%23kosmos-dev@server).
+ return this.id.replace(/^#(?=[^#])/, '');
}
- get shortName () {
+ get sidebarName () {
switch (this.protocol) {
case 'IRC':
- return this.name.replace(/#/g,'');
+ // Strip a single leading '#' so the sidebar (which already renders a
+ // leading '#') shows all hashes of the channel name, e.g.
+ // #kosmos-dev -> kosmos-dev (renders as "# kosmos-dev"),
+ // ##kosmos-dev -> #kosmos-dev (renders as "# #kosmos-dev").
+ return this.name.replace(/^#/, '');
case 'XMPP':
return this.name.match(/^(.+)@/)[1];
default:
diff --git a/app/models/channel.js b/app/models/channel.js
index a2a7fa36..6ee20c29 100644
--- a/app/models/channel.js
+++ b/app/models/channel.js
@@ -24,8 +24,13 @@ export default class Channel extends BaseChannel {
}
get publicLogsBaseUrl () {
- // TODO needs to get hostname from channel meta info for federated protocols
- return `${config.publicLogs.defaultBaseUrl}/${this.account.server.hostname.toLowerCase()}/channels/${this.shortName}`;
+ // Mirror the slug's leading-# rule: strip a single leading '#' only for
+ // single-# channels (keeps existing log URLs working); preserve all '#' for
+ // multi-# channels and percent-encode them so they don't become URL fragments.
+ const channelName = this.name
+ .replace(/^#(?=[^#])/, '')
+ .replace(/#/g, '%23');
+ return `${config.publicLogs.defaultBaseUrl}/${this.account.server.hostname.toLowerCase()}/channels/${channelName}`;
}
}
diff --git a/app/routes/base_channel.js b/app/routes/base_channel.js
index 21c0e74a..65fcb8f1 100644
--- a/app/routes/base_channel.js
+++ b/app/routes/base_channel.js
@@ -13,15 +13,15 @@ export default class BaseChannelRoute extends Route {
}
model (params) {
- let channel = this.coms.channels.find(ch => ch.slug === params.slug);
+ const slug = decodeURIComponent(params.slug);
+ let channel = this.coms.channels.find(ch => ch.slug === slug);
if (channel) return channel;
- const channelId = decodeURIComponent(params.slug);
- const domain = channelId.match(/@([^/]+)/)[1];
+ const domain = slug.match(/@([^/]+)/)[1];
const randomChannelForDomain = this.coms.channels.find(ch => ch.domain === domain);
if (randomChannelForDomain) {
- channel = this.createChannelOrUserChannel(randomChannelForDomain.account, channelId);
+ channel = this.createChannelOrUserChannel(randomChannelForDomain.account, slug);
return channel;
} else {
const firstChannel = this.coms.channels.firstObject;
diff --git a/app/routes/channel.js b/app/routes/channel.js
index d5c123ed..5509f2d5 100644
--- a/app/routes/channel.js
+++ b/app/routes/channel.js
@@ -5,9 +5,14 @@ export default class ChannelRoute extends BaseChannel {
createChannelOrUserChannel (account, channelId) {
let channel;
switch(account.protocol) {
- case 'IRC':
- channel = this.coms.createChannel(account, '#'+channelId.match(/^(.+)@/)[1]);
+ case 'IRC': {
+ let name = channelId.match(/^(.+)@/)[1];
+ // Old/persisted slugs for single-hash channels have no leading '#';
+ // multi-hash slugs already carry their '#' characters.
+ if (!name.startsWith('#')) name = `#${name}`;
+ channel = this.coms.createChannel(account, name);
break;
+ }
case 'XMPP':
channel = this.coms.createChannel(account, channelId);
break;
diff --git a/app/services/sockethub-irc.js b/app/services/sockethub-irc.js
index c2458755..f95f019d 100644
--- a/app/services/sockethub-irc.js
+++ b/app/services/sockethub-irc.js
@@ -91,7 +91,7 @@ export default class SockethubIrcService extends Service {
}
handlePresenceUpdate (message) {
- const match = message.target.id.match(/(.+)\//);
+ const match = message.target.id.match(/@(.+)$/);
if (!match) { console.warn('Could not parse hostname from presence message', message); return; }
let channel = this.coms.channels.find(ch => ch.sockethubChannelId === message.target.id);
diff --git a/tests/unit/models/base-channel-test.js b/tests/unit/models/base-channel-test.js
index 284030c0..d944e9ea 100644
--- a/tests/unit/models/base-channel-test.js
+++ b/tests/unit/models/base-channel-test.js
@@ -18,6 +18,53 @@ module('Unit | Model | base-channel', function (hooks) {
assert.strictEqual(model.slug, 'kosmos-dev@irc.libera.chat');
});
+ test('#slug strips only one leading hash for multi-hash channels', function (assert) {
+ const model = new BaseChannel({
+ account: ircAccount,
+ name: '##kosmos-dev'
+ });
+
+ assert.strictEqual(model.slug, '##kosmos-dev@irc.libera.chat');
+ });
+
+ test('#id and #sockethubChannelId preserve all hashes for IRC channels', function (assert) {
+ let model = new BaseChannel({
+ account: ircAccount,
+ name: '#kosmos-dev'
+ });
+ assert.strictEqual(model.id, '#kosmos-dev@irc.libera.chat', 'id preserves single hash');
+ assert.strictEqual(model.sockethubChannelId, '#kosmos-dev@irc.libera.chat', 'sockethubChannelId matches id');
+
+ model = new BaseChannel({
+ account: ircAccount,
+ name: '##kosmos-dev'
+ });
+ assert.strictEqual(model.id, '##kosmos-dev@irc.libera.chat', 'id preserves all hashes');
+ assert.strictEqual(model.sockethubChannelId, '##kosmos-dev@irc.libera.chat', 'sockethubChannelId matches id');
+ });
+
+ test('#sidebarName (IRC) strips a single leading hash', function (assert) {
+ let model = new BaseChannel({
+ account: ircAccount,
+ name: '#kosmos-dev'
+ });
+ assert.strictEqual(model.sidebarName, 'kosmos-dev', 'strips the single leading hash');
+
+ model = new BaseChannel({
+ account: ircAccount,
+ name: '##kosmos-dev'
+ });
+ assert.strictEqual(model.sidebarName, '#kosmos-dev', 'preserves remaining hashes');
+ });
+
+ test('#sidebarName (XMPP) returns the local part', function (assert) {
+ const model = new BaseChannel({
+ account: xmppAccount,
+ name: 'kosmos-dev@kosmos.chat'
+ });
+ assert.strictEqual(model.sidebarName, 'kosmos-dev');
+ });
+
//
// unreadMessagesClass
//
diff --git a/tests/unit/models/channel-test.js b/tests/unit/models/channel-test.js
index e9a34740..968f8bf3 100644
--- a/tests/unit/models/channel-test.js
+++ b/tests/unit/models/channel-test.js
@@ -26,17 +26,21 @@ module('Unit | Model | channel', function (hooks) {
assert.strictEqual(channel.formattedTopic.toString(), 'never gonna <marquee>give you up</marquee>');
});
- test('#shortName', function (assert) {
- let channel = new Channel({
+ test('#publicLogsBaseUrl keeps single-hash channels unencoded', function (assert) {
+ const channel = new Channel({
account: ircAccount,
name: '#kosmos-dev'
});
- assert.strictEqual(channel.shortName, 'kosmos-dev', 'returns name without hash for IRC');
+ assert.strictEqual(channel.publicLogsBaseUrl, 'https://storage.5apps.com/kosmos/public/chat-messages/irc.libera.chat/channels/kosmos-dev');
+ });
- channel = new Channel({
- account: xmppAccount,
- name: 'kosmos-dev@kosmos.chat'
+ test('#publicLogsBaseUrl percent-encodes all hashes for multi-hash channels', function (assert) {
+ const channel = new Channel({
+ account: ircAccount,
+ name: '##kosmos-dev'
});
- assert.strictEqual(channel.shortName, 'kosmos-dev', 'returns name without MUC domain for XMPP');
+ const baseUrl = channel.publicLogsBaseUrl;
+ assert.ok(!baseUrl.includes('#'), 'logs URL has no fragment');
+ assert.strictEqual(baseUrl, 'https://storage.5apps.com/kosmos/public/chat-messages/irc.libera.chat/channels/%23%23kosmos-dev');
});
});
diff --git a/tests/unit/routes/channel-test.js b/tests/unit/routes/channel-test.js
new file mode 100644
index 00000000..4cc52fb6
--- /dev/null
+++ b/tests/unit/routes/channel-test.js
@@ -0,0 +1,53 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+import Service from '@ember/service';
+import ChannelRoute from 'hyperchannel/routes/channel';
+import Channel from 'hyperchannel/models/channel';
+import { ircAccount } from '../../fixtures/accounts';
+
+// Stubs the coms service so createChannelOrUserChannel's channelName argument
+// can be captured without joining a real channel. Returns a real Channel
+// instance so id/sockethubChannelId getters resolve.
+class ComsStub extends Service {
+ createChannelCalls = [];
+ createChannel (account, channelName) {
+ this.createChannelCalls.push({ account, channelName });
+ return new Channel({ account, name: channelName });
+ }
+}
+
+module('Unit | Route | channel', function (hooks) {
+ setupTest(hooks);
+
+ hooks.beforeEach(function () {
+ this.owner.register('service:coms', ComsStub);
+ });
+
+ test('createChannelOrUserChannel reconstructs a single-hash IRC channel name', function (assert) {
+ const route = new ChannelRoute();
+ route.coms = this.owner.lookup('service:coms');
+
+ // Slug for #kosmos-dev (single leading hash stripped) -> kosmos-dev@server
+ const channel = route.createChannelOrUserChannel(ircAccount, 'kosmos-dev@irc.libera.chat');
+
+ assert.strictEqual(route.coms.createChannelCalls.length, 1);
+ assert.strictEqual(route.coms.createChannelCalls[0].channelName, '#kosmos-dev');
+ assert.strictEqual(channel.name, '#kosmos-dev');
+ assert.strictEqual(channel.id, '#kosmos-dev@irc.libera.chat');
+ assert.strictEqual(channel.sockethubChannelId, '#kosmos-dev@irc.libera.chat');
+ });
+
+ test('createChannelOrUserChannel reconstructs a multi-hash IRC channel name', function (assert) {
+ const route = new ChannelRoute();
+ route.coms = this.owner.lookup('service:coms');
+
+ // Slug for ##kosmos-dev (all hashes preserved) -> ##kosmos-dev@server
+ const channel = route.createChannelOrUserChannel(ircAccount, '##kosmos-dev@irc.libera.chat');
+
+ assert.strictEqual(route.coms.createChannelCalls.length, 1);
+ assert.strictEqual(route.coms.createChannelCalls[0].channelName, '##kosmos-dev');
+ assert.strictEqual(channel.name, '##kosmos-dev');
+ assert.strictEqual(channel.id, '##kosmos-dev@irc.libera.chat');
+ assert.strictEqual(channel.sockethubChannelId, '##kosmos-dev@irc.libera.chat');
+ });
+});
diff --git a/tests/unit/services/sockethub-irc-test.js b/tests/unit/services/sockethub-irc-test.js
index e6ade7db..8a1d0762 100644
--- a/tests/unit/services/sockethub-irc-test.js
+++ b/tests/unit/services/sockethub-irc-test.js
@@ -15,6 +15,27 @@ module('Unit | Service | sockethub irc', function (hooks) {
assert.ok(channel.connected);
});
+ test('#handlePresenceUpdate extracts hostname from the name@host ID format and adds the user', function (assert) {
+ const channel = new Channel({
+ account: ircAccount,
+ name: '##kosmos-dev'
+ });
+ const service = this.owner.lookup('service:sockethub-irc');
+ service.coms = {
+ channels: [channel],
+ accounts: [ircAccount]
+ };
+
+ service.handlePresenceUpdate({
+ target: { id: '##kosmos-dev@irc.libera.chat', type: 'room' },
+ actor: { id: 'newuser@irc.libera.chat', name: 'newuser' }
+ });
+
+ assert.ok(channel.connected, 'marks the channel connected');
+ assert.ok(channel.userList.includes('newuser'), 'adds the incoming user');
+ assert.ok(channel.userList.includes(ircAccount.nickname), 'adds the own nickname');
+ });
+
// FIXME this test randomly fails with error "Assertion occured after test had finished."
// skip('#join sends the join activity to Sockethub for a room channel', function(assert) {
// const done = assert.async();