Skip to content

Commit

Permalink
spellchecker: Use Electron 8 built-in spellchecker.
Browse files Browse the repository at this point in the history
* Using electron built-in spellchecker
* Added the custom context menu

Co-authored-by: Anders Kaseorg <[email protected]>

Fixes: #504
  • Loading branch information
manavmehta authored Jun 18, 2020
1 parent 4261874 commit 0fff633
Show file tree
Hide file tree
Showing 12 changed files with 248 additions and 303 deletions.
4 changes: 3 additions & 1 deletion app/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,9 @@ app.on('activate', () => {
app.on('ready', () => {
const ses = session.fromPartition('persist:webviewsession');
ses.setUserAgent(`ZulipElectron/${app.getVersion()} ${ses.getUserAgent()}`);

ipcMain.on('set-spellcheck-langs', () => {
ses.setSpellCheckerLanguages(ConfigUtil.getConfigItem('spellcheckerLanguages'));
});
AppMenu.setMenu({
tabs: []
});
Expand Down
102 changes: 102 additions & 0 deletions app/renderer/js/components/context-menu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {remote, ContextMenuParams} from 'electron';
import * as t from '../utils/translation-util';
const {clipboard, Menu} = remote;

export const contextMenu = (webContents: Electron.WebContents, event: Event, props: ContextMenuParams) => {
const isText = Boolean(props.selectionText.length);
const isLink = Boolean(props.linkURL);

const makeSuggestion = (suggestion: string) => ({
label: suggestion,
visible: true,
async click() {
await webContents.insertText(suggestion);
}
});

let menuTemplate: Electron.MenuItemConstructorOptions[] = [{
label: t.__('Add to Dictionary'),
visible: props.isEditable && isText && props.misspelledWord.length !== 0,
click(_item) {
webContents.session.addWordToSpellCheckerDictionary(props.misspelledWord);
}
}, {
type: 'separator'
}, {
label: `${t.__('Look Up')} "${props.selectionText}"`,
visible: process.platform === 'darwin' && isText,
click(_item) {
webContents.showDefinitionForSelection();
}
}, {
type: 'separator'
}, {
label: t.__('Cut'),
visible: isText,
enabled: props.isEditable,
accelerator: 'CommandOrControl+X',
click(_item) {
webContents.cut();
}
}, {
label: t.__('Copy'),
accelerator: 'CommandOrControl+C',
click(_item) {
webContents.copy();
}
}, {
label: t.__('Paste'), // Bug: Paste replaces text
accelerator: 'CommandOrControl+V',
enabled: props.isEditable,
click() {
webContents.paste();
}
}, {
type: 'separator'
}, {
label: t.__('Copy Link'),
visible: isText && isLink,
click(_item) {
clipboard.write({
bookmark: props.linkText,
text: props.linkURL
});
}
}, {
label: t.__('Copy Image'),
visible: props.mediaType === 'image',
click(_item) {
webContents.copyImageAt(props.x, props.y);
}
}, {
label: t.__('Copy Image URL'),
visible: props.mediaType === 'image',
click(_item) {
clipboard.write({
bookmark: props.srcURL,
text: props.srcURL
});
}
}, {
type: 'separator'
}, {
label: t.__('Services'),
visible: process.platform === 'darwin',
role: 'services'
}];

if (props.misspelledWord) {
if (props.dictionarySuggestions.length > 0) {
const suggestions: Electron.MenuItemConstructorOptions[] = props.dictionarySuggestions.map((suggestion: string) => makeSuggestion(suggestion));
menuTemplate = suggestions.concat(menuTemplate);
} else {
menuTemplate.unshift({
label: t.__('No Suggestion Found'),
enabled: false
});
}
}

const menu = Menu.buildFromTemplate(menuTemplate);
menu.popup();
};
12 changes: 11 additions & 1 deletion app/renderer/js/components/webview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as ConfigUtil from '../utils/config-util';
import * as SystemUtil from '../utils/system-util';
import BaseComponent from './base';
import handleExternalLink from './handle-external-link';
import {contextMenu} from './context-menu';

const {app, dialog} = remote;

Expand Down Expand Up @@ -57,7 +58,11 @@ export default class WebView extends BaseComponent {
${this.props.preload ? 'preload="js/preload.js"' : ''}
partition="persist:webviewsession"
name="${this.props.name}"
webpreferences="${this.props.nodeIntegration ? '' : 'contextIsolation, '}javascript=yes">
webpreferences="
${this.props.nodeIntegration ? '' : 'contextIsolation,'}
${ConfigUtil.getConfigItem('enableSpellchecker') ? 'spellcheck,' : ''}
javascript
">
</webview>`;
}

Expand Down Expand Up @@ -117,6 +122,11 @@ export default class WebView extends BaseComponent {
});

this.$el.addEventListener('dom-ready', () => {
const webContents = remote.webContents.fromId(this.$el.getWebContentsId());
webContents.addListener('context-menu', (event, menuParameters) => {
contextMenu(webContents, event, menuParameters);
});

if (this.props.role === 'server') {
this.$el.classList.add('onload');
}
Expand Down
4 changes: 1 addition & 3 deletions app/renderer/js/injected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,7 @@ interface CompatElectronBridge extends ElectronBridge {

const {page_params} = zulipWindow;
if (page_params) {
electron_bridge.send_event('zulip-loaded', {
serverLanguage: page_params.default_language
});
electron_bridge.send_event('zulip-loaded');
}
})();

Expand Down
3 changes: 3 additions & 0 deletions app/renderer/js/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ interface SettingsOptions extends DNDSettings {
quitOnClose: boolean;
promptDownload: boolean;
dockBouncing?: boolean;
spellcheckerLanguages?: string[];
}

const logger = new Logger({
Expand Down Expand Up @@ -137,6 +138,7 @@ class ServerManagerView {
await this.initTabs();
this.initActions();
this.registerIpcs();
ipcRenderer.send('set-spellcheck-langs');
}

async loadProxy(): Promise<void> {
Expand Down Expand Up @@ -214,6 +216,7 @@ class ServerManagerView {

if (process.platform !== 'darwin') {
settingOptions.autoHideMenubar = false;
settingOptions.spellcheckerLanguages = ['en-US'];
}

for (const [setting, value] of Object.entries(settingOptions)) {
Expand Down
87 changes: 86 additions & 1 deletion app/renderer/js/pages/preference/general-section.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import {ipcRenderer, remote, OpenDialogOptions} from 'electron';
import path from 'path';
import fs from 'fs-extra';

const {app, dialog} = remote;
const {app, dialog, session} = remote;
const currentBrowserWindow = remote.getCurrentWindow();

import BaseSection from './base-section';
import * as ConfigUtil from '../../utils/config-util';
import * as EnterpriseUtil from '../../utils/enterprise-util';
import * as t from '../../utils/translation-util';
import supportedLocales from '../../../../translations/supported-locales.json';
import Tagify from '@yaireo/tagify';
import ISO6391 from 'iso-639-1';

interface GeneralSectionProps {
$root: Element;
Expand Down Expand Up @@ -93,7 +95,10 @@ export default class GeneralSection extends BaseSection {
<div class="setting-description">${t.__('Enable spellchecker (requires restart)')}</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="spellcheck-langs" style= "display:${process.platform === 'darwin' ? 'none' : ''}"></div>
<div class="setting-row" id="note"></div>
</div>
<div class="title">${t.__('Advanced')}</div>
<div class="settings-card">
Expand Down Expand Up @@ -171,6 +176,7 @@ export default class GeneralSection extends BaseSection {
this.updatePromptDownloadOption();
this.enableErrorReporting();
this.setLocale();
this.initSpellChecker();

// Platform specific settings

Expand Down Expand Up @@ -356,6 +362,10 @@ export default class GeneralSection extends BaseSection {
const newValue = !ConfigUtil.getConfigItem('enableSpellchecker');
ConfigUtil.setConfigItem('enableSpellchecker', newValue);
this.enableSpellchecker();
const spellcheckerLanguageInput: HTMLElement = document.querySelector('#spellcheck-langs');
const spellcheckerNote: HTMLElement = document.querySelector('#note');
spellcheckerLanguageInput.style.display = spellcheckerLanguageInput.style.display === 'none' ? '' : 'none';
spellcheckerNote.style.display = spellcheckerNote.style.display === 'none' ? '' : 'none';
}
});
}
Expand Down Expand Up @@ -493,4 +503,79 @@ export default class GeneralSection extends BaseSection {
await this.factoryResetSettings();
});
}

initSpellChecker(): void {
// The elctron API is a no-op on macOS and macOS default spellchecker is used.
if (process.platform === 'darwin') {
const note: HTMLElement = document.querySelector('#note');
note.append(t.__('On macOS, the OS spellchecker is used.'));
note.append(document.createElement('br'));
note.append(t.__('Change the language from System Preferences → Keyboard → Text → Spelling.'));
} else {
const note: HTMLElement = document.querySelector('#note');
note.append(t.__('You can select a maximum of 3 languages for spellchecking.'));
const spellDiv: HTMLElement = document.querySelector('#spellcheck-langs');
spellDiv.innerHTML += `
<div class="setting-description">${t.__('Spellchecker Languages')}</div>
<input name='spellcheck' placeholder='Enter Languages'>`;

const availableLanguages = session.fromPartition('persist:webviewsession').availableSpellCheckerLanguages;
let languagePairs: Map<string, string> = new Map();
availableLanguages.forEach((l: string) => {
if (ISO6391.validate(l)) {
languagePairs.set(ISO6391.getName(l), l);
}
});

// Manually set names for languages not available in ISO6391
languagePairs.set('English (AU)', 'en-AU');
languagePairs.set('English (CA)', 'en-CA');
languagePairs.set('English (GB)', 'en-GB');
languagePairs.set('English (US)', 'en-US');
languagePairs.set('Spanish (Latin America)', 'es-419');
languagePairs.set('Spanish (Argentina)', 'es-AR');
languagePairs.set('Spanish (Mexico)', 'es-MX');
languagePairs.set('Spanish (US)', 'es-US');
languagePairs.set('Portuguese (Brazil)', 'pt-BR');
languagePairs.set('Portuguese (Portugal)', 'pt-PT');
languagePairs.set('Serbo-Croatian', 'sh');

languagePairs = new Map([...languagePairs].sort((a, b) => ((a[0] < b[0]) ? -1 : 1)));

const tagField: HTMLElement = document.querySelector('input[name=spellcheck]');
const tagify = new Tagify(tagField, {
whitelist: [...languagePairs.keys()],
enforceWhitelist: true,
maxTags: 3,
dropdown: {
enabled: 0,
maxItems: Infinity,
closeOnSelect: false,
highlightFirst: true
}
});

const configuredLanguages: string[] = ConfigUtil.getConfigItem('spellcheckerLanguages').map((code: string) => [...languagePairs].filter(pair => (pair[1] === code))[0][0]);
tagify.addTags(configuredLanguages);

tagField.addEventListener('change', event => {
if ((event.target as HTMLInputElement).value.length === 0) {
ConfigUtil.setConfigItem('spellcheckerLanguages', []);
ipcRenderer.send('set-spellcheck-langs');
} else {
const spellLangs: string[] = [...JSON.parse((event.target as HTMLInputElement).value).values()].map(elt => languagePairs.get(elt.value));
ConfigUtil.setConfigItem('spellcheckerLanguages', spellLangs);
ipcRenderer.send('set-spellcheck-langs');
}
});
}

// Do not display the spellchecker input and note if it is disabled
if (!ConfigUtil.getConfigItem('enableSpellchecker')) {
const spellcheckerLanguageInput: HTMLElement = document.querySelector('#spellcheck-langs');
const spellcheckerNote: HTMLElement = document.querySelector('#note');
spellcheckerLanguageInput.style.display = 'none';
spellcheckerNote.style.display = 'none';
}
}
}
15 changes: 1 addition & 14 deletions app/renderer/js/preload.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {contextBridge, ipcRenderer, webFrame} from 'electron';
import fs from 'fs';
import * as SetupSpellChecker from './spellchecker';

import isDev from 'electron-is-dev';

Expand Down Expand Up @@ -54,13 +53,7 @@ ipcRenderer.on('show-notification-settings', () => {
}, 100);
});

electron_bridge.once('zulip-loaded', ({serverLanguage}) => {
// Get the default language of the server
if (serverLanguage) {
// Init spellchecker
SetupSpellChecker.init(serverLanguage);
}

electron_bridge.once('zulip-loaded', () => {
// Redirect users to network troubleshooting page
const getRestartButton = document.querySelector('.restart_get_events_button');
if (getRestartButton) {
Expand All @@ -70,12 +63,6 @@ electron_bridge.once('zulip-loaded', ({serverLanguage}) => {
}
});

// Clean up spellchecker events after you navigate away from this page;
// otherwise, you may experience errors
window.addEventListener('beforeunload', (): void => {
SetupSpellChecker.unsubscribeSpellChecker();
});

window.addEventListener('load', (event: any): void => {
if (!event.target.URL.includes('app/renderer/network.html')) {
return;
Expand Down
Loading

0 comments on commit 0fff633

Please sign in to comment.