FEATURE: Add keyboard shortcuts for jumping to unread channels (#29734)

This commit is contained in:
Gary Pendergast 2024-11-18 11:18:58 +11:00 committed by GitHub
parent f700a72c8f
commit 69d9868c7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 181 additions and 10 deletions

View File

@ -28,7 +28,11 @@ function buildHTML(keys1, keys2, keysDelimiter, shortcutsDelimiter) {
const allKeys = [keys1, keys2]
.reject((keys) => keys.length === 0)
.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;
@ -40,13 +44,22 @@ function buildHTML(keys1, keys2, keysDelimiter, shortcutsDelimiter) {
return I18n.t(`${KEY}.shortcut_delimiter_slash`, { shortcut1, shortcut2 });
} else if (shortcutsDelimiter === "space") {
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) {
return `<span dir="ltr">${shortcut}</span>`;
function wrapInSpan(shortcut, delimiter) {
return `<span class="delimiter-${delimiter}" dir="ltr">${shortcut}</span>`;
}
function buildShortcut(

View File

@ -105,6 +105,11 @@
margin-left: auto;
}
.delimiter-newline {
display: revert;
text-align: right;
}
kbd {
font-family: var(--font-family);
font-weight: bold;

View File

@ -4479,6 +4479,7 @@ en:
shortcut_delimiter_or: "%{shortcut1} or %{shortcut2}"
shortcut_delimiter_slash: "%{shortcut1}/%{shortcut2}"
shortcut_delimiter_space: "%{shortcut1} %{shortcut2}"
shortcut_delimiter_newline: "%{shortcut1}<br>%{shortcut2}"
title: "Keyboard Shortcuts"
short_title: "Shortcuts"
jump_to:

View File

@ -40,6 +40,18 @@ export default {
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) =>
el.classList.contains("chat-composer__input");
const isInputSelection = (el) => {
@ -156,6 +168,25 @@ export default {
api.addKeyboardShortcut("alt+down", handleMoveDownShortcut, {
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", "&uarr;"],
keys2: ["alt", "shift", "&darr;"],
keysDelimiter: "plus",
shortcutsDelimiter: "newline",
},
},
});
api.addKeyboardShortcut("alt+shift+down", handleMoveDownUnreadShortcut, {
global: true,
});
api.addKeyboardShortcut(
`${PLATFORM_KEY_MODIFIER}+b`,
(event) => modifyComposerSelection(event, "bold"),

View File

@ -131,6 +131,18 @@ export default class ChatChannelsManager extends Service {
.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() {
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() {
return this.directMessageChannels.slice(0, DIRECT_MESSAGE_CHANNELS_LIMIT);
}

View File

@ -278,19 +278,31 @@ export default class Chat extends Service {
: 0;
}
switchChannelUpOrDown(direction) {
switchChannelUpOrDown(direction, unreadOnly = false) {
const { activeChannel } = this;
if (!activeChannel) {
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;
if (activeChannel.isDirectMessageChannel) {
currentList = this.chatChannelsManager.truncatedDirectMessageChannels;
otherList = this.chatChannelsManager.publicMessageChannels;
currentList = directChannels;
otherList = publicChannels;
} else {
currentList = this.chatChannelsManager.publicMessageChannels;
otherList = this.chatChannelsManager.truncatedDirectMessageChannels;
currentList = publicChannels;
otherList = directChannels;
}
const directionUp = direction === "up";

View File

@ -725,6 +725,7 @@ en:
title: "Chat"
keyboard_shortcuts:
switch_channel_arrows: "%{shortcut} Switch channel"
switch__unread_channel_arrows: "%{shortcut} Switch unread channel"
open_quick_channel_selector: "%{shortcut} Open quick channel selector"
open_insert_link_modal: "%{shortcut} Insert hyperlink (composer only)"
composer_bold: "%{shortcut} Bold (composer only)"

View File

@ -11,7 +11,7 @@ RSpec.describe "Shortcuts | sidebar", type: :system do
sign_in(current_user)
end
context "when using Up/Down arrows" do
context "when using Alt+Up/Down arrows" do
fab!(:channel_1) { Fabricate(:chat_channel) }
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
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