Keyboard shortcuts and accessibility
This commit is contained in:
parent
8590a047c7
commit
20a892247f
87 changed files with 3652 additions and 711 deletions
|
@ -16,7 +16,7 @@ if (window.getAppInstance()) {
|
|||
$('.environment').text(states.join(' - '));
|
||||
|
||||
// Install the 'dismiss with escape key' handler
|
||||
$(document).on('keyup', e => {
|
||||
$(document).on('keydown', e => {
|
||||
'use strict';
|
||||
|
||||
if (e.keyCode === 27) {
|
||||
|
|
511
js/background.js
511
js/background.js
|
@ -282,6 +282,8 @@
|
|||
$('.dark-overlay').on('click', () => $('.dark-overlay').remove());
|
||||
},
|
||||
removeDarkOverlay: () => $('.dark-overlay').remove(),
|
||||
showKeyboardShortcuts: () => window.showKeyboardShortcuts(),
|
||||
|
||||
deleteAllData: () => {
|
||||
const clearDataView = new window.Whisper.ClearDataView().render();
|
||||
$('body').append(clearDataView.el);
|
||||
|
@ -518,6 +520,7 @@
|
|||
tempPath: window.baseTempPath,
|
||||
regionCode: window.storage.get('regionCode'),
|
||||
ourNumber: textsecure.storage.user.getNumber(),
|
||||
platform: window.platform,
|
||||
i18n: window.i18n,
|
||||
},
|
||||
};
|
||||
|
@ -581,26 +584,498 @@
|
|||
Whisper.events.on('messageExpired', messageExpired);
|
||||
Whisper.events.on('userChanged', userChanged);
|
||||
|
||||
// In the future this listener will be added by the conversation view itself. But
|
||||
// because we currently have multiple converations open at once, we install just
|
||||
// one global handler.
|
||||
// $(document).on('keydown', event => {
|
||||
// const { ctrlKey, key } = event;
|
||||
let shortcutGuideView = null;
|
||||
|
||||
// We can add Command-E as the Mac shortcut when we add it to our Electron menus:
|
||||
// https://stackoverflow.com/questions/27380018/when-cmd-key-is-kept-pressed-keyup-is-not-triggered-for-any-other-key
|
||||
// For now, it will stay as CTRL-E only
|
||||
// if (key === 'e' && ctrlKey) {
|
||||
// const state = store.getState();
|
||||
// const selectedId = state.conversations.selectedConversation;
|
||||
// const conversation = ConversationController.get(selectedId);
|
||||
window.showKeyboardShortcuts = () => {
|
||||
if (!shortcutGuideView) {
|
||||
shortcutGuideView = new Whisper.ReactWrapperView({
|
||||
className: 'shortcut-guide-wrapper',
|
||||
JSX: Signal.State.Roots.createShortcutGuideModal(window.reduxStore, {
|
||||
close: () => {
|
||||
if (shortcutGuideView) {
|
||||
shortcutGuideView.remove();
|
||||
shortcutGuideView = null;
|
||||
}
|
||||
},
|
||||
}),
|
||||
onClose: () => {
|
||||
shortcutGuideView = null;
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// if (conversation && !conversation.get('isArchived')) {
|
||||
// conversation.setArchived(true);
|
||||
// conversation.trigger('unload');
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
function findConversation(conversationId, direction, unreadOnly) {
|
||||
const state = store.getState();
|
||||
const lists = Signal.State.Selectors.conversations.getLeftPaneLists(
|
||||
state
|
||||
);
|
||||
const toSearch = state.conversations.showArchived
|
||||
? lists.archivedConversations
|
||||
: lists.conversations;
|
||||
|
||||
const increment = direction === 'up' ? -1 : 1;
|
||||
let startIndex;
|
||||
|
||||
if (conversationId) {
|
||||
const index = toSearch.findIndex(item => item.id === conversationId);
|
||||
if (index >= 0) {
|
||||
startIndex = index + increment;
|
||||
}
|
||||
} else {
|
||||
startIndex = direction === 'up' ? toSearch.length - 1 : 0;
|
||||
}
|
||||
|
||||
for (
|
||||
let i = startIndex, max = toSearch.length;
|
||||
i >= 0 && i < max;
|
||||
i += increment
|
||||
) {
|
||||
const target = toSearch[i];
|
||||
if (!unreadOnly) {
|
||||
return target.id;
|
||||
} else if (target.unreadCount > 0) {
|
||||
return target.id;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', event => {
|
||||
const { altKey, ctrlKey, key, metaKey, shiftKey } = event;
|
||||
|
||||
const optionOrAlt = altKey;
|
||||
const ctrlOrCommand = metaKey || ctrlKey;
|
||||
|
||||
const state = store.getState();
|
||||
const selectedId = state.conversations.selectedConversation;
|
||||
const conversation = ConversationController.get(selectedId);
|
||||
|
||||
// NAVIGATION
|
||||
|
||||
// Show keyboard shortcuts - handled by Electron-managed keyboard shortcuts
|
||||
|
||||
// Navigate by section
|
||||
if (ctrlOrCommand && !shiftKey && key === 't') {
|
||||
const focusedElement = document.activeElement;
|
||||
|
||||
const targets = [
|
||||
document.querySelector('.module-main-header .module-avatar-button'),
|
||||
document.querySelector('.module-left-pane__to-inbox-button'),
|
||||
document.querySelector('.module-main-header__search__input'),
|
||||
document.querySelector('.module-left-pane__list'),
|
||||
document.querySelector('.module-search-results'),
|
||||
document.querySelector(
|
||||
'.module-composition-area .public-DraftEditor-content'
|
||||
),
|
||||
];
|
||||
const focusedIndex = targets.findIndex(target => {
|
||||
if (!target || !focusedElement) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target === focusedElement) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (target.contains(focusedElement)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
const lastIndex = targets.length - 1;
|
||||
|
||||
let index;
|
||||
if (focusedIndex < 0 || focusedIndex >= lastIndex) {
|
||||
index = 0;
|
||||
} else {
|
||||
index = focusedIndex + 1;
|
||||
}
|
||||
|
||||
while (!targets[index]) {
|
||||
index += 1;
|
||||
if (index > lastIndex) {
|
||||
index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
targets[index].focus();
|
||||
}
|
||||
|
||||
// Cancel out of keyboard shortcut screen - has first precedence
|
||||
if (shortcutGuideView && key === 'Escape') {
|
||||
shortcutGuideView.remove();
|
||||
shortcutGuideView = null;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Escape is heavily overloaded - here we avoid clashes with other Escape handlers
|
||||
if (key === 'Escape') {
|
||||
// Check origin - if within a react component which handles escape, don't handle.
|
||||
// Why? Because React's synthetic events can cause events to be handled twice.
|
||||
const target = document.activeElement;
|
||||
|
||||
if (
|
||||
target &&
|
||||
target.attributes &&
|
||||
target.attributes.class &&
|
||||
target.attributes.class.value
|
||||
) {
|
||||
const className = target.attributes.class.value;
|
||||
|
||||
// These want to handle events internally
|
||||
|
||||
// CaptionEditor text box
|
||||
if (className.includes('module-caption-editor__caption-input')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// MainHeader search box
|
||||
if (className.includes('module-main-header__search__input')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// These add listeners to document, but we'll run first
|
||||
const confirmationModal = document.querySelector(
|
||||
'.module-confirmation-dialog__overlay'
|
||||
);
|
||||
if (confirmationModal) {
|
||||
return;
|
||||
}
|
||||
|
||||
const emojiPicker = document.querySelector('.module-emoji-picker');
|
||||
if (emojiPicker) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lightBox = document.querySelector('.module-lightbox');
|
||||
if (lightBox) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stickerPicker = document.querySelector('.module-sticker-picker');
|
||||
if (stickerPicker) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stickerPreview = document.querySelector(
|
||||
'.module-sticker-manager__preview-modal__overlay'
|
||||
);
|
||||
if (stickerPreview) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Close Backbone-based confirmation dialog
|
||||
if (Whisper.activeConfirmationView && key === 'Escape') {
|
||||
Whisper.activeConfirmationView.remove();
|
||||
Whisper.activeConfirmationView = null;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Send Escape to active conversation so it can close panels
|
||||
if (conversation && key === 'Escape') {
|
||||
conversation.trigger('escape-pressed');
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Change currently selected conversation - up/down, to next/previous unread
|
||||
if (optionOrAlt && !shiftKey && key === 'ArrowUp') {
|
||||
const unreadOnly = false;
|
||||
const targetId = findConversation(
|
||||
conversation ? conversation.id : null,
|
||||
'up',
|
||||
unreadOnly
|
||||
);
|
||||
|
||||
if (targetId) {
|
||||
window.Whisper.events.trigger('showConversation', targetId);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (optionOrAlt && !shiftKey && key === 'ArrowDown') {
|
||||
const unreadOnly = false;
|
||||
const targetId = findConversation(
|
||||
conversation ? conversation.id : null,
|
||||
'down',
|
||||
unreadOnly
|
||||
);
|
||||
|
||||
if (targetId) {
|
||||
window.Whisper.events.trigger('showConversation', targetId);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (optionOrAlt && shiftKey && key === 'ArrowUp') {
|
||||
const unreadOnly = true;
|
||||
const targetId = findConversation(
|
||||
conversation ? conversation.id : null,
|
||||
'up',
|
||||
unreadOnly
|
||||
);
|
||||
|
||||
if (targetId) {
|
||||
window.Whisper.events.trigger('showConversation', targetId);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (optionOrAlt && shiftKey && key === 'ArrowDown') {
|
||||
const unreadOnly = true;
|
||||
const targetId = findConversation(
|
||||
conversation ? conversation.id : null,
|
||||
'down',
|
||||
unreadOnly
|
||||
);
|
||||
|
||||
if (targetId) {
|
||||
window.Whisper.events.trigger('showConversation', targetId);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Preferences - handled by Electron-managed keyboard shortcuts
|
||||
|
||||
// Open the top-right menu for current conversation
|
||||
if (conversation && ctrlOrCommand && shiftKey && key === 'i') {
|
||||
const button = document.querySelector(
|
||||
'.module-conversation-header__more-button'
|
||||
);
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Because the menu is shown at a location based on the initiating click, we need
|
||||
// to fake up a mouse event to get the menu to show somewhere other than (0,0).
|
||||
const { x, y, width, height } = button.getBoundingClientRect();
|
||||
const mouseEvent = document.createEvent('MouseEvents');
|
||||
mouseEvent.initMouseEvent(
|
||||
'click',
|
||||
true, // bubbles
|
||||
false, // cancelable
|
||||
null, // view
|
||||
null, // detail
|
||||
0, // screenX,
|
||||
0, // screenY,
|
||||
x + width / 2,
|
||||
y + height / 2,
|
||||
false, // ctrlKey,
|
||||
false, // altKey,
|
||||
false, // shiftKey,
|
||||
false, // metaKey,
|
||||
false, // button,
|
||||
document.body
|
||||
);
|
||||
|
||||
button.dispatchEvent(mouseEvent);
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Search
|
||||
if (ctrlOrCommand && !shiftKey && key === 'f') {
|
||||
const { startSearch } = actions.search;
|
||||
startSearch();
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Search in conversation
|
||||
if (conversation && ctrlOrCommand && shiftKey && key === 'f') {
|
||||
const { searchInConversation } = actions.search;
|
||||
const name = conversation.isMe()
|
||||
? window.i18n('noteToSelf')
|
||||
: conversation.getTitle();
|
||||
searchInConversation(conversation.id, name);
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Focus composer field
|
||||
if (conversation && ctrlOrCommand && shiftKey && key === 't') {
|
||||
conversation.trigger('focus-composer');
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Open all media
|
||||
if (conversation && ctrlOrCommand && shiftKey && key === 'm') {
|
||||
conversation.trigger('open-all-media');
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Open emoji picker - handled by component
|
||||
|
||||
// Open sticker picker - handled by component
|
||||
|
||||
// Begin recording voice note
|
||||
if (conversation && ctrlOrCommand && shiftKey && key === 'v') {
|
||||
conversation.trigger('begin-recording');
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Archive or unarchive conversation
|
||||
if (
|
||||
conversation &&
|
||||
!conversation.get('isArchived') &&
|
||||
ctrlOrCommand &&
|
||||
shiftKey &&
|
||||
key === 'a'
|
||||
) {
|
||||
conversation.setArchived(true);
|
||||
conversation.trigger('unload', 'keyboard shortcut archive');
|
||||
Whisper.ToastView.show(
|
||||
Whisper.ConversationArchivedToast,
|
||||
document.body
|
||||
);
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
conversation &&
|
||||
conversation.get('isArchived') &&
|
||||
ctrlOrCommand &&
|
||||
shiftKey &&
|
||||
key === 'u'
|
||||
) {
|
||||
conversation.setArchived(false);
|
||||
Whisper.ToastView.show(
|
||||
Whisper.ConversationUnarchivedToast,
|
||||
document.body
|
||||
);
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Scroll to bottom of list - handled by component
|
||||
|
||||
// Scroll to top of list - handled by component
|
||||
|
||||
// Close conversation
|
||||
if (conversation && ctrlOrCommand && shiftKey && key === 'c') {
|
||||
conversation.trigger('unload', 'keyboard shortcut close');
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// MESSAGES
|
||||
|
||||
// Show message details
|
||||
if (conversation && ctrlOrCommand && !shiftKey && key === 'd') {
|
||||
const { selectedMessage } = state.conversations;
|
||||
if (!selectedMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
conversation.trigger('show-message-details', selectedMessage);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle reply to message
|
||||
if (conversation && ctrlOrCommand && shiftKey && key === 'r') {
|
||||
const { selectedMessage } = state.conversations;
|
||||
|
||||
conversation.trigger('toggle-reply', selectedMessage);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Save attachment
|
||||
if (conversation && ctrlOrCommand && !shiftKey && key === 's') {
|
||||
const { selectedMessage } = state.conversations;
|
||||
|
||||
if (selectedMessage) {
|
||||
conversation.trigger('save-attachment', selectedMessage);
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (conversation && ctrlOrCommand && shiftKey && key === 'd') {
|
||||
const { selectedMessage } = state.conversations;
|
||||
|
||||
if (selectedMessage) {
|
||||
conversation.trigger('delete-message', selectedMessage);
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// COMPOSER
|
||||
|
||||
// Create a newline in your message - handled by component
|
||||
|
||||
// Expand composer - handled by component
|
||||
|
||||
// Send in expanded composer - handled by component
|
||||
|
||||
// Attach file
|
||||
if (conversation && ctrlOrCommand && !shiftKey && key === 'u') {
|
||||
conversation.trigger('attach-file');
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove draft link preview
|
||||
if (conversation && ctrlOrCommand && !shiftKey && key === 'p') {
|
||||
conversation.trigger('remove-link-review');
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Attach file
|
||||
if (conversation && ctrlOrCommand && shiftKey && key === 'p') {
|
||||
conversation.trigger('remove-all-draft-attachments');
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
// Commented out because this is the last item
|
||||
// return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Whisper.events.on('setupWithImport', () => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* global $: false */
|
||||
/* global Whisper: false */
|
||||
|
||||
$(document).on('keyup', e => {
|
||||
$(document).on('keydown', e => {
|
||||
'use strict';
|
||||
|
||||
if (e.keyCode === 27) {
|
||||
|
|
|
@ -125,6 +125,17 @@
|
|||
};
|
||||
},
|
||||
|
||||
isNormalBubble() {
|
||||
return (
|
||||
!this.isUnsupportedMessage() &&
|
||||
!this.isExpirationTimerUpdate() &&
|
||||
!this.isKeyChange() &&
|
||||
!this.isVerifiedChange() &&
|
||||
!this.isGroupUpdate() &&
|
||||
!this.isEndSession()
|
||||
);
|
||||
},
|
||||
|
||||
// Top-level prop generation for the message bubble
|
||||
getPropsForBubble() {
|
||||
if (this.isUnsupportedMessage()) {
|
||||
|
@ -489,6 +500,7 @@
|
|||
text: this.createNonBreakingLastSeparator(this.get('body')),
|
||||
textPending: this.get('bodyPending'),
|
||||
id: this.id,
|
||||
conversationId: this.get('conversationId'),
|
||||
isSticker: Boolean(sticker),
|
||||
direction: this.isIncoming() ? 'incoming' : 'outgoing',
|
||||
timestamp: this.get('sent_at'),
|
||||
|
|
|
@ -57,6 +57,9 @@ const {
|
|||
const {
|
||||
createStickerPreviewModal,
|
||||
} = require('../../ts/state/roots/createStickerPreviewModal');
|
||||
const {
|
||||
createShortcutGuideModal,
|
||||
} = require('../../ts/state/roots/createShortcutGuideModal');
|
||||
|
||||
const { createStore } = require('../../ts/state/createStore');
|
||||
const conversationsDuck = require('../../ts/state/ducks/conversations');
|
||||
|
@ -66,6 +69,8 @@ const searchDuck = require('../../ts/state/ducks/search');
|
|||
const stickersDuck = require('../../ts/state/ducks/stickers');
|
||||
const userDuck = require('../../ts/state/ducks/user');
|
||||
|
||||
const conversationsSelectors = require('../../ts/state/selectors/conversations');
|
||||
|
||||
// Migrations
|
||||
const {
|
||||
getPlaceholderMigrations,
|
||||
|
@ -266,9 +271,10 @@ exports.setup = (options = {}) => {
|
|||
const Roots = {
|
||||
createCompositionArea,
|
||||
createLeftPane,
|
||||
createTimeline,
|
||||
createShortcutGuideModal,
|
||||
createStickerManager,
|
||||
createStickerPreviewModal,
|
||||
createTimeline,
|
||||
};
|
||||
const Ducks = {
|
||||
conversations: conversationsDuck,
|
||||
|
@ -278,11 +284,16 @@ exports.setup = (options = {}) => {
|
|||
search: searchDuck,
|
||||
stickers: stickersDuck,
|
||||
};
|
||||
const Selectors = {
|
||||
conversations: conversationsSelectors,
|
||||
};
|
||||
|
||||
const State = {
|
||||
bindActionCreators,
|
||||
createStore,
|
||||
Roots,
|
||||
Ducks,
|
||||
Selectors,
|
||||
};
|
||||
|
||||
const Types = {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* global $, Whisper, i18n */
|
||||
|
||||
$(document).on('keyup', e => {
|
||||
$(document).on('keydown', e => {
|
||||
'use strict';
|
||||
|
||||
if (e.keyCode === 27) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* global $, Whisper */
|
||||
|
||||
$(document).on('keyup', e => {
|
||||
$(document).on('keydown', e => {
|
||||
'use strict';
|
||||
|
||||
if (e.keyCode === 27) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global Whisper, i18n */
|
||||
/* global Backbone, Whisper, i18n */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
|
@ -10,6 +10,8 @@
|
|||
className: 'confirmation-dialog modal',
|
||||
templateName: 'confirmation-dialog',
|
||||
initialize(options) {
|
||||
this.previousFocus = document.activeElement;
|
||||
|
||||
this.message = options.message;
|
||||
this.hideCancel = options.hideCancel;
|
||||
|
||||
|
@ -19,13 +21,26 @@
|
|||
this.reject = options.reject;
|
||||
this.cancelText = options.cancelText || i18n('cancel');
|
||||
|
||||
if (Whisper.activeConfirmationView) {
|
||||
Whisper.activeConfirmationView.remove();
|
||||
Whisper.activeConfirmationView = null;
|
||||
}
|
||||
|
||||
Whisper.activeConfirmationView = this;
|
||||
|
||||
this.render();
|
||||
},
|
||||
events: {
|
||||
keyup: 'onKeyup',
|
||||
keydown: 'onKeydown',
|
||||
'click .ok': 'ok',
|
||||
'click .cancel': 'cancel',
|
||||
},
|
||||
remove() {
|
||||
if (this.previousFocus && this.previousFocus.focus) {
|
||||
this.previousFocus.focus();
|
||||
}
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
},
|
||||
render_attributes() {
|
||||
return {
|
||||
message: this.message,
|
||||
|
@ -46,9 +61,12 @@
|
|||
this.reject();
|
||||
}
|
||||
},
|
||||
onKeyup(event) {
|
||||
onKeydown(event) {
|
||||
if (event.key === 'Escape' || event.key === 'Esc') {
|
||||
this.cancel();
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
focusCancel() {
|
||||
|
|
|
@ -77,6 +77,16 @@
|
|||
return { toastMessage: i18n('voiceNoteMustBeOnlyAttachment') };
|
||||
},
|
||||
});
|
||||
Whisper.ConversationArchivedToast = Whisper.ToastView.extend({
|
||||
render_attributes() {
|
||||
return { toastMessage: i18n('conversationArchived') };
|
||||
},
|
||||
});
|
||||
Whisper.ConversationUnarchivedToast = Whisper.ToastView.extend({
|
||||
render_attributes() {
|
||||
return { toastMessage: i18n('conversationReturnedToInbox') };
|
||||
},
|
||||
});
|
||||
|
||||
const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
|
||||
Whisper.MessageBodyTooLongToast = Whisper.ToastView.extend({
|
||||
|
@ -143,6 +153,28 @@
|
|||
this.listenTo(this.model, 'unload', reason =>
|
||||
this.unload(`model trigger - ${reason}`)
|
||||
);
|
||||
this.listenTo(this.model, 'focus-composer', this.focusMessageField);
|
||||
this.listenTo(this.model, 'open-all-media', this.showAllMedia);
|
||||
this.listenTo(this.model, 'begin-recording', this.captureAudio);
|
||||
this.listenTo(this.model, 'attach-file', this.onChooseAttachment);
|
||||
this.listenTo(this.model, 'escape-pressed', this.resetPanel);
|
||||
this.listenTo(this.model, 'show-message-details', this.showMessageDetail);
|
||||
this.listenTo(this.model, 'toggle-reply', messageId => {
|
||||
const target = this.quote || !messageId ? null : messageId;
|
||||
this.setQuoteMessage(target);
|
||||
});
|
||||
this.listenTo(
|
||||
this.model,
|
||||
'save-attachment',
|
||||
this.downloadAttachmentWrapper
|
||||
);
|
||||
this.listenTo(this.model, 'delete-message', this.deleteMessage);
|
||||
this.listenTo(this.model, 'remove-link-review', this.removeLinkPreview);
|
||||
this.listenTo(
|
||||
this.model,
|
||||
'remove-all-draft-attachments',
|
||||
this.clearAttachments
|
||||
);
|
||||
|
||||
// Events on Message models - we still listen to these here because they
|
||||
// can be emitted by the non-reduxified MessageDetail pane
|
||||
|
@ -272,9 +304,8 @@
|
|||
onShowSafetyNumber: () => {
|
||||
this.showSafetyNumber();
|
||||
},
|
||||
onShowAllMedia: async () => {
|
||||
await this.showAllMedia();
|
||||
this.updateHeader();
|
||||
onShowAllMedia: () => {
|
||||
this.showAllMedia();
|
||||
},
|
||||
onShowGroupMembers: async () => {
|
||||
await this.showMembers();
|
||||
|
@ -282,15 +313,24 @@
|
|||
},
|
||||
onGoBack: () => {
|
||||
this.resetPanel();
|
||||
this.updateHeader();
|
||||
},
|
||||
|
||||
onArchive: () => {
|
||||
this.model.trigger('unload', 'archive');
|
||||
this.model.setArchived(true);
|
||||
this.model.trigger('unload', 'archive');
|
||||
|
||||
Whisper.ToastView.show(
|
||||
Whisper.ConversationArchivedToast,
|
||||
document.body
|
||||
);
|
||||
},
|
||||
onMoveToInbox: () => {
|
||||
this.model.setArchived(false);
|
||||
|
||||
Whisper.ToastView.show(
|
||||
Whisper.ConversationUnarchivedToast,
|
||||
document.body
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -655,12 +695,13 @@
|
|||
|
||||
const cleaned = await this.cleanModels(all);
|
||||
this.model.messageCollection.reset(cleaned);
|
||||
const scrollToMessageId = disableScroll ? undefined : messageId;
|
||||
|
||||
messagesReset(
|
||||
conversationId,
|
||||
cleaned.map(model => model.getReduxData()),
|
||||
metrics,
|
||||
disableScroll ? undefined : messageId
|
||||
scrollToMessageId
|
||||
);
|
||||
} catch (error) {
|
||||
setMessagesLoading(conversationId, false);
|
||||
|
@ -670,7 +711,7 @@
|
|||
}
|
||||
},
|
||||
|
||||
async loadNewestMessages(newestMessageId) {
|
||||
async loadNewestMessages(newestMessageId, setFocus) {
|
||||
const {
|
||||
messagesReset,
|
||||
setMessagesLoading,
|
||||
|
@ -701,7 +742,9 @@
|
|||
const metrics = await getMessageMetricsForConversation(conversationId);
|
||||
|
||||
if (scrollToLatestUnread && metrics.oldestUnread) {
|
||||
this.loadAndScroll(metrics.oldestUnread.id, { disableScroll: true });
|
||||
this.loadAndScroll(metrics.oldestUnread.id, {
|
||||
disableScroll: !setFocus,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -712,11 +755,14 @@
|
|||
|
||||
const cleaned = await this.cleanModels(messages);
|
||||
this.model.messageCollection.reset(cleaned);
|
||||
const scrollToMessageId =
|
||||
setFocus && metrics.newest ? metrics.newest.id : undefined;
|
||||
|
||||
messagesReset(
|
||||
conversationId,
|
||||
cleaned.map(model => model.getReduxData()),
|
||||
metrics
|
||||
metrics,
|
||||
scrollToMessageId
|
||||
);
|
||||
} catch (error) {
|
||||
setMessagesLoading(conversationId, false);
|
||||
|
@ -1485,7 +1531,13 @@
|
|||
},
|
||||
|
||||
captureAudio(e) {
|
||||
e.preventDefault();
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (this.compositionApi.current.isDirty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.hasFiles()) {
|
||||
this.showToast(Whisper.VoiceNoteMustBeOnlyAttachmentToast);
|
||||
|
@ -1507,6 +1559,7 @@
|
|||
view.on('send', this.handleAudioCapture.bind(this));
|
||||
view.on('closed', this.endCaptureAudio.bind(this));
|
||||
view.$el.appendTo(this.$('.capture-audio'));
|
||||
view.$('.finish').focus();
|
||||
this.compositionApi.current.setMicActive(true);
|
||||
|
||||
this.disableMessageField();
|
||||
|
@ -1527,12 +1580,18 @@
|
|||
flags: textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE,
|
||||
};
|
||||
|
||||
// Note: The RecorderView removes itself on send
|
||||
this.captureAudioView = null;
|
||||
|
||||
this.sendMessage();
|
||||
},
|
||||
endCaptureAudio() {
|
||||
this.enableMessageField();
|
||||
this.$('.microphone').show();
|
||||
|
||||
// Note: The RecorderView removes itself on close
|
||||
this.captureAudioView = null;
|
||||
|
||||
this.compositionApi.current.setMicActive(false);
|
||||
},
|
||||
|
||||
|
@ -1727,6 +1786,7 @@
|
|||
this.listenTo(this.model.messageCollection, 'remove', update);
|
||||
|
||||
this.listenBack(view);
|
||||
this.updateHeader();
|
||||
},
|
||||
|
||||
focusMessageField() {
|
||||
|
@ -1835,6 +1895,27 @@
|
|||
}
|
||||
},
|
||||
|
||||
downloadAttachmentWrapper(messageId) {
|
||||
const message = this.model.messageCollection.get(messageId);
|
||||
if (!message) {
|
||||
throw new Error(
|
||||
`downloadAttachmentWrapper: Did not find message for id ${messageId}`
|
||||
);
|
||||
}
|
||||
|
||||
const { attachments, sent_at: timestamp } = message.attributes;
|
||||
if (!attachments || attachments.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attachment = attachments[0];
|
||||
const { fileName } = attachment;
|
||||
|
||||
const isDangerous = window.Signal.Util.isFileDangerous(fileName || '');
|
||||
|
||||
this.downloadAttachment({ attachment, timestamp, isDangerous });
|
||||
},
|
||||
|
||||
downloadAttachment({ attachment, timestamp, isDangerous }) {
|
||||
if (isDangerous) {
|
||||
this.showToast(Whisper.DangerousFileTypeToast);
|
||||
|
@ -1944,7 +2025,6 @@
|
|||
message.trigger('unload');
|
||||
this.model.messageCollection.remove(message.id);
|
||||
this.resetPanel();
|
||||
this.updateHeader();
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -2081,10 +2161,13 @@
|
|||
);
|
||||
}
|
||||
|
||||
if (!message.isNormalBubble()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
this.stopListening(message, 'change', update);
|
||||
this.resetPanel();
|
||||
this.updateHeader();
|
||||
};
|
||||
|
||||
const props = message.getPropsForMessageDetail();
|
||||
|
@ -2111,7 +2194,6 @@
|
|||
JSX: Signal.State.Roots.createStickerManager(window.reduxStore),
|
||||
onClose: () => {
|
||||
this.resetPanel();
|
||||
this.updateHeader();
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -2135,7 +2217,6 @@
|
|||
},
|
||||
onClose: () => {
|
||||
this.resetPanel();
|
||||
this.updateHeader();
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -2149,6 +2230,11 @@
|
|||
|
||||
listenBack(view) {
|
||||
this.panels = this.panels || [];
|
||||
|
||||
if (this.panels.length === 0) {
|
||||
this.previousFocus = document.activeElement;
|
||||
}
|
||||
|
||||
this.panels.unshift(view);
|
||||
view.$el.insertAfter(this.$('.panel').last());
|
||||
view.$el.one('animationend', () => {
|
||||
|
@ -2162,11 +2248,23 @@
|
|||
|
||||
const view = this.panels.shift();
|
||||
|
||||
if (
|
||||
this.panels.length === 0 &&
|
||||
this.previousFocus &&
|
||||
this.previousFocus.focus
|
||||
) {
|
||||
this.previousFocus.focus();
|
||||
this.previousFocus = null;
|
||||
}
|
||||
|
||||
if (this.panels.length > 0) {
|
||||
this.panels[0].$el.fadeIn(250);
|
||||
}
|
||||
this.updateHeader();
|
||||
|
||||
view.$el.addClass('panel--remove').one('transitionend', () => {
|
||||
view.remove();
|
||||
|
||||
if (this.panels.length === 0) {
|
||||
// Make sure poppers are positioned properly
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
|
@ -2290,8 +2388,19 @@
|
|||
},
|
||||
|
||||
async setQuoteMessage(messageId) {
|
||||
const model = messageId
|
||||
? await getMessageById(messageId, {
|
||||
Message: Whisper.Message,
|
||||
})
|
||||
: null;
|
||||
|
||||
if (model && !model.isNormalBubble()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.quote = null;
|
||||
this.quotedMessage = null;
|
||||
this.quoteHolder = null;
|
||||
|
||||
const existing = this.model.get('quotedMessageId');
|
||||
if (existing !== messageId) {
|
||||
|
@ -2303,25 +2412,20 @@
|
|||
await this.saveModel();
|
||||
}
|
||||
|
||||
if (this.quoteHolder) {
|
||||
this.quoteHolder.unload();
|
||||
this.quoteHolder = null;
|
||||
if (this.quoteView) {
|
||||
this.quoteView.remove();
|
||||
this.quoteView = null;
|
||||
}
|
||||
|
||||
if (messageId) {
|
||||
const model = await getMessageById(messageId, {
|
||||
Message: Whisper.Message,
|
||||
});
|
||||
if (model) {
|
||||
const message = MessageController.register(model.id, model);
|
||||
this.quotedMessage = message;
|
||||
if (model) {
|
||||
const message = MessageController.register(model.id, model);
|
||||
this.quotedMessage = message;
|
||||
|
||||
if (message) {
|
||||
const quote = await this.model.makeQuote(this.quotedMessage);
|
||||
this.quote = quote;
|
||||
if (message) {
|
||||
const quote = await this.model.makeQuote(this.quotedMessage);
|
||||
this.quote = quote;
|
||||
|
||||
this.focusMessageFieldAndClearDisabled();
|
||||
}
|
||||
this.focusMessageFieldAndClearDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2363,6 +2467,8 @@
|
|||
props: Object.assign({}, props, {
|
||||
withContentAbove: true,
|
||||
onClose: () => {
|
||||
// This can't be the normal 'onClose' because that is always run when this
|
||||
// view is removed from the DOM, and would clear the draft quote.
|
||||
this.setQuoteMessage(null);
|
||||
},
|
||||
}),
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
);
|
||||
view.$el.appendTo(this.el);
|
||||
|
||||
if (this.lastConversation) {
|
||||
if (this.lastConversation && this.lastConversation !== conversation) {
|
||||
this.lastConversation.trigger(
|
||||
'unload',
|
||||
'opened another conversation'
|
||||
|
@ -171,6 +171,13 @@
|
|||
if (view) {
|
||||
this.appLoadingScreen = null;
|
||||
view.remove();
|
||||
|
||||
const searchInput = document.querySelector(
|
||||
'.module-main-header__search__input'
|
||||
);
|
||||
if (searchInput && searchInput.focus) {
|
||||
searchInput.focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
onProgress(count) {
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
},
|
||||
events: {
|
||||
change: 'validateNumber',
|
||||
keyup: 'validateNumber',
|
||||
keydown: 'validateNumber',
|
||||
},
|
||||
validateNumber() {
|
||||
const input = this.$('input.number');
|
||||
|
|
|
@ -18,6 +18,9 @@
|
|||
this.onSwitchAwayBound = this.onSwitchAway.bind(this);
|
||||
$(window).on('blur', this.onSwitchAwayBound);
|
||||
|
||||
this.handleKeyDownBound = this.handleKeyDown.bind(this);
|
||||
this.$el.on('keydown', this.handleKeyDownBound);
|
||||
|
||||
this.start();
|
||||
},
|
||||
events: {
|
||||
|
@ -28,6 +31,14 @@
|
|||
onSwitchAway() {
|
||||
this.close();
|
||||
},
|
||||
handleKeyDown(event) {
|
||||
if (event.key === 'Escape') {
|
||||
this.close();
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
updateTime() {
|
||||
const duration = moment.duration(Date.now() - this.startTime, 'ms');
|
||||
const minutes = `${Math.trunc(duration.asMinutes())}`;
|
||||
|
@ -67,6 +78,8 @@
|
|||
this.trigger('closed');
|
||||
|
||||
$(window).off('blur', this.onSwitchAwayBound);
|
||||
|
||||
this.$el.off('keydown', this.handleKeyDownBound);
|
||||
},
|
||||
finish() {
|
||||
this.clickedFinish = true;
|
||||
|
|
|
@ -28,4 +28,10 @@
|
|||
setTimeout(this.close.bind(this), 2000);
|
||||
},
|
||||
});
|
||||
|
||||
Whisper.ToastView.show = (View, el) => {
|
||||
const toast = new View();
|
||||
toast.$el.appendTo(el);
|
||||
toast.render();
|
||||
};
|
||||
})();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue