FEATURE: Add keyboard shortcuts for jumping to unread channels (#29734)
This commit is contained in:
parent
f700a72c8f
commit
69d9868c7f
|
@ -28,7 +28,11 @@ function buildHTML(keys1, keys2, keysDelimiter, shortcutsDelimiter) {
|
||||||
const allKeys = [keys1, keys2]
|
const allKeys = [keys1, keys2]
|
||||||
.reject((keys) => keys.length === 0)
|
.reject((keys) => keys.length === 0)
|
||||||
.map((keys) => keys.map((k) => `<kbd>${k}</kbd>`).join(keysDelimiter))
|
.map((keys) => keys.map((k) => `<kbd>${k}</kbd>`).join(keysDelimiter))
|
||||||
.map((keys) => (shortcutsDelimiter !== "space" ? wrapInSpan(keys) : keys));
|
.map((keys) =>
|
||||||
|
shortcutsDelimiter !== "space" && shortcutsDelimiter !== "newline"
|
||||||
|
? wrapInSpan(keys, shortcutsDelimiter)
|
||||||
|
: keys
|
||||||
|
);
|
||||||
|
|
||||||
const [shortcut1, shortcut2] = allKeys;
|
const [shortcut1, shortcut2] = allKeys;
|
||||||
|
|
||||||
|
@ -40,13 +44,22 @@ function buildHTML(keys1, keys2, keysDelimiter, shortcutsDelimiter) {
|
||||||
return I18n.t(`${KEY}.shortcut_delimiter_slash`, { shortcut1, shortcut2 });
|
return I18n.t(`${KEY}.shortcut_delimiter_slash`, { shortcut1, shortcut2 });
|
||||||
} else if (shortcutsDelimiter === "space") {
|
} else if (shortcutsDelimiter === "space") {
|
||||||
return wrapInSpan(
|
return wrapInSpan(
|
||||||
I18n.t(`${KEY}.shortcut_delimiter_space`, { shortcut1, shortcut2 })
|
I18n.t(`${KEY}.shortcut_delimiter_space`, { shortcut1, shortcut2 }),
|
||||||
|
shortcutsDelimiter
|
||||||
|
);
|
||||||
|
} else if (shortcutsDelimiter === "newline") {
|
||||||
|
return wrapInSpan(
|
||||||
|
I18n.t(`${KEY}.shortcut_delimiter_newline`, {
|
||||||
|
shortcut1,
|
||||||
|
shortcut2,
|
||||||
|
}),
|
||||||
|
shortcutsDelimiter
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrapInSpan(shortcut) {
|
function wrapInSpan(shortcut, delimiter) {
|
||||||
return `<span dir="ltr">${shortcut}</span>`;
|
return `<span class="delimiter-${delimiter}" dir="ltr">${shortcut}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildShortcut(
|
function buildShortcut(
|
||||||
|
|
|
@ -105,6 +105,11 @@
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.delimiter-newline {
|
||||||
|
display: revert;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
kbd {
|
kbd {
|
||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
|
@ -4479,6 +4479,7 @@ en:
|
||||||
shortcut_delimiter_or: "%{shortcut1} or %{shortcut2}"
|
shortcut_delimiter_or: "%{shortcut1} or %{shortcut2}"
|
||||||
shortcut_delimiter_slash: "%{shortcut1}/%{shortcut2}"
|
shortcut_delimiter_slash: "%{shortcut1}/%{shortcut2}"
|
||||||
shortcut_delimiter_space: "%{shortcut1} %{shortcut2}"
|
shortcut_delimiter_space: "%{shortcut1} %{shortcut2}"
|
||||||
|
shortcut_delimiter_newline: "%{shortcut1}<br>%{shortcut2}"
|
||||||
title: "Keyboard Shortcuts"
|
title: "Keyboard Shortcuts"
|
||||||
short_title: "Shortcuts"
|
short_title: "Shortcuts"
|
||||||
jump_to:
|
jump_to:
|
||||||
|
|
|
@ -40,6 +40,18 @@ export default {
|
||||||
chatService.switchChannelUpOrDown("down");
|
chatService.switchChannelUpOrDown("down");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMoveUpUnreadShortcut = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
chatService.switchChannelUpOrDown("up", true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMoveDownUnreadShortcut = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
chatService.switchChannelUpOrDown("down", true);
|
||||||
|
};
|
||||||
|
|
||||||
const isChatComposer = (el) =>
|
const isChatComposer = (el) =>
|
||||||
el.classList.contains("chat-composer__input");
|
el.classList.contains("chat-composer__input");
|
||||||
const isInputSelection = (el) => {
|
const isInputSelection = (el) => {
|
||||||
|
@ -156,6 +168,25 @@ export default {
|
||||||
api.addKeyboardShortcut("alt+down", handleMoveDownShortcut, {
|
api.addKeyboardShortcut("alt+down", handleMoveDownShortcut, {
|
||||||
global: true,
|
global: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
api.addKeyboardShortcut("alt+shift+up", handleMoveUpUnreadShortcut, {
|
||||||
|
global: true,
|
||||||
|
help: {
|
||||||
|
category: "chat",
|
||||||
|
name: "chat.keyboard_shortcuts.switch__unread_channel_arrows",
|
||||||
|
definition: {
|
||||||
|
keys1: ["alt", "shift", "↑"],
|
||||||
|
keys2: ["alt", "shift", "↓"],
|
||||||
|
keysDelimiter: "plus",
|
||||||
|
shortcutsDelimiter: "newline",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
api.addKeyboardShortcut("alt+shift+down", handleMoveDownUnreadShortcut, {
|
||||||
|
global: true,
|
||||||
|
});
|
||||||
|
|
||||||
api.addKeyboardShortcut(
|
api.addKeyboardShortcut(
|
||||||
`${PLATFORM_KEY_MODIFIER}+b`,
|
`${PLATFORM_KEY_MODIFIER}+b`,
|
||||||
(event) => modifyComposerSelection(event, "bold"),
|
(event) => modifyComposerSelection(event, "bold"),
|
||||||
|
|
|
@ -131,6 +131,18 @@ export default class ChatChannelsManager extends Service {
|
||||||
.sort((a, b) => a?.slug?.localeCompare?.(b?.slug));
|
.sort((a, b) => a?.slug?.localeCompare?.(b?.slug));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@cached
|
||||||
|
get publicMessageChannelsWithActivity() {
|
||||||
|
return this.publicMessageChannels.filter(
|
||||||
|
(channel) =>
|
||||||
|
channel.tracking.unreadCount +
|
||||||
|
channel.tracking.mentionCount +
|
||||||
|
channel.tracking.watchedThreadsUnreadCount +
|
||||||
|
channel.threadsManager.unreadThreadCount >
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
get publicMessageChannelsByActivity() {
|
get publicMessageChannelsByActivity() {
|
||||||
return this.#sortChannelsByActivity(this.publicMessageChannels);
|
return this.#sortChannelsByActivity(this.publicMessageChannels);
|
||||||
}
|
}
|
||||||
|
@ -145,6 +157,18 @@ export default class ChatChannelsManager extends Service {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@cached
|
||||||
|
get directMessageChannelsWithActivity() {
|
||||||
|
return this.directMessageChannels.filter(
|
||||||
|
(channel) =>
|
||||||
|
channel.tracking.unreadCount +
|
||||||
|
channel.tracking.mentionCount +
|
||||||
|
channel.tracking.watchedThreadsUnreadCount +
|
||||||
|
channel.threadsManager.unreadThreadCount >
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
get truncatedDirectMessageChannels() {
|
get truncatedDirectMessageChannels() {
|
||||||
return this.directMessageChannels.slice(0, DIRECT_MESSAGE_CHANNELS_LIMIT);
|
return this.directMessageChannels.slice(0, DIRECT_MESSAGE_CHANNELS_LIMIT);
|
||||||
}
|
}
|
||||||
|
|
|
@ -278,19 +278,31 @@ export default class Chat extends Service {
|
||||||
: 0;
|
: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
switchChannelUpOrDown(direction) {
|
switchChannelUpOrDown(direction, unreadOnly = false) {
|
||||||
const { activeChannel } = this;
|
const { activeChannel } = this;
|
||||||
if (!activeChannel) {
|
if (!activeChannel) {
|
||||||
return; // Chat isn't open. Return and do nothing!
|
return; // Chat isn't open. Return and do nothing!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let publicChannels, directChannels;
|
||||||
|
|
||||||
|
if (unreadOnly) {
|
||||||
|
publicChannels =
|
||||||
|
this.chatChannelsManager.publicMessageChannelsWithActivity;
|
||||||
|
directChannels =
|
||||||
|
this.chatChannelsManager.directMessageChannelsWithActivity;
|
||||||
|
} else {
|
||||||
|
publicChannels = this.chatChannelsManager.publicMessageChannels;
|
||||||
|
directChannels = this.chatChannelsManager.directMessageChannels;
|
||||||
|
}
|
||||||
|
|
||||||
let currentList, otherList;
|
let currentList, otherList;
|
||||||
if (activeChannel.isDirectMessageChannel) {
|
if (activeChannel.isDirectMessageChannel) {
|
||||||
currentList = this.chatChannelsManager.truncatedDirectMessageChannels;
|
currentList = directChannels;
|
||||||
otherList = this.chatChannelsManager.publicMessageChannels;
|
otherList = publicChannels;
|
||||||
} else {
|
} else {
|
||||||
currentList = this.chatChannelsManager.publicMessageChannels;
|
currentList = publicChannels;
|
||||||
otherList = this.chatChannelsManager.truncatedDirectMessageChannels;
|
otherList = directChannels;
|
||||||
}
|
}
|
||||||
|
|
||||||
const directionUp = direction === "up";
|
const directionUp = direction === "up";
|
||||||
|
|
|
@ -725,6 +725,7 @@ en:
|
||||||
title: "Chat"
|
title: "Chat"
|
||||||
keyboard_shortcuts:
|
keyboard_shortcuts:
|
||||||
switch_channel_arrows: "%{shortcut} Switch channel"
|
switch_channel_arrows: "%{shortcut} Switch channel"
|
||||||
|
switch__unread_channel_arrows: "%{shortcut} Switch unread channel"
|
||||||
open_quick_channel_selector: "%{shortcut} Open quick channel selector"
|
open_quick_channel_selector: "%{shortcut} Open quick channel selector"
|
||||||
open_insert_link_modal: "%{shortcut} Insert hyperlink (composer only)"
|
open_insert_link_modal: "%{shortcut} Insert hyperlink (composer only)"
|
||||||
composer_bold: "%{shortcut} Bold (composer only)"
|
composer_bold: "%{shortcut} Bold (composer only)"
|
||||||
|
|
|
@ -11,7 +11,7 @@ RSpec.describe "Shortcuts | sidebar", type: :system do
|
||||||
sign_in(current_user)
|
sign_in(current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when using Up/Down arrows" do
|
context "when using Alt+Up/Down arrows" do
|
||||||
fab!(:channel_1) { Fabricate(:chat_channel) }
|
fab!(:channel_1) { Fabricate(:chat_channel) }
|
||||||
fab!(:dm_channel_1) { Fabricate(:direct_message_channel, users: [current_user]) }
|
fab!(:dm_channel_1) { Fabricate(:direct_message_channel, users: [current_user]) }
|
||||||
|
|
||||||
|
@ -47,4 +47,88 @@ RSpec.describe "Shortcuts | sidebar", type: :system do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "when using Alt+Shift+Up/Down arrows" do
|
||||||
|
fab!(:channel_1) { Fabricate(:chat_channel) }
|
||||||
|
fab!(:channel_2) { Fabricate(:chat_channel) }
|
||||||
|
fab!(:dm_channel_1) { Fabricate(:direct_message_channel, users: [current_user]) }
|
||||||
|
fab!(:dm_channel_2) { Fabricate(:direct_message_channel, users: [current_user]) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
channel_1.add(current_user)
|
||||||
|
channel_2.add(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when on homepage" do
|
||||||
|
it "does nothing" do
|
||||||
|
visit("/")
|
||||||
|
find("body").send_keys(%i[alt shift arrow_down])
|
||||||
|
|
||||||
|
expect(page).to have_no_selector(".channel-#{channel_1.id}.active")
|
||||||
|
expect(page).to have_no_selector(".channel-#{channel_2.id}.active")
|
||||||
|
expect(page).to have_no_selector(".channel-#{dm_channel_1.id}.active")
|
||||||
|
expect(page).to have_no_selector(".channel-#{dm_channel_2.id}.active")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when on chat page" do
|
||||||
|
it "does nothing when no channels have activity" do
|
||||||
|
chat.visit_channel(channel_1)
|
||||||
|
|
||||||
|
expect(page).to have_selector(".channel-#{channel_1.id}.active")
|
||||||
|
|
||||||
|
find("body").send_keys(%i[alt shift arrow_down])
|
||||||
|
|
||||||
|
expect(page).to have_selector(".channel-#{channel_1.id}.active")
|
||||||
|
|
||||||
|
find("body").send_keys(%i[alt shift arrow_down])
|
||||||
|
|
||||||
|
expect(page).to have_selector(".channel-#{channel_1.id}.active")
|
||||||
|
|
||||||
|
find("body").send_keys(%i[alt shift arrow_up])
|
||||||
|
|
||||||
|
expect(page).to have_selector(".channel-#{channel_1.id}.active")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "navigates through the channels with activity" do
|
||||||
|
Fabricate(:chat_message, chat_channel: channel_2, message: "hello!", use_service: true)
|
||||||
|
|
||||||
|
Fabricate(
|
||||||
|
:chat_message,
|
||||||
|
chat_channel: dm_channel_2,
|
||||||
|
message: "hello here!",
|
||||||
|
use_service: true,
|
||||||
|
)
|
||||||
|
|
||||||
|
chat.visit_channel(channel_1)
|
||||||
|
|
||||||
|
expect(page).to have_selector(".channel-#{channel_1.id}.active")
|
||||||
|
|
||||||
|
find("body").send_keys(%i[alt shift arrow_down])
|
||||||
|
|
||||||
|
expect(page).to have_selector(".channel-#{channel_2.id}.active")
|
||||||
|
|
||||||
|
find("body").send_keys(%i[alt shift arrow_down])
|
||||||
|
|
||||||
|
expect(page).to have_selector(".channel-#{dm_channel_2.id}.active")
|
||||||
|
|
||||||
|
Fabricate(
|
||||||
|
:chat_message,
|
||||||
|
chat_channel: channel_1,
|
||||||
|
message: "hello again!",
|
||||||
|
use_service: true,
|
||||||
|
)
|
||||||
|
|
||||||
|
find("body").send_keys(%i[alt shift arrow_up])
|
||||||
|
|
||||||
|
expect(page).to have_selector(".channel-#{channel_1.id}.active")
|
||||||
|
|
||||||
|
Fabricate(:chat_message, chat_channel: dm_channel_1, message: "bye now!", use_service: true)
|
||||||
|
|
||||||
|
find("body").send_keys(%i[alt shift arrow_up])
|
||||||
|
|
||||||
|
expect(page).to have_selector(".channel-#{dm_channel_1.id}.active")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue