diff --git a/CHANGELOG.md b/CHANGELOG.md index 764c989612..bb63ba5acc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,12 @@ - add button to clear chat history (delete all messages of a chat) - add recently seen indicator - add jump down button +- add "search for messages in chat" - show webxdc icon in quote ## Changed -- start migrating to jsonrpc api +- migrated core communication to jsonrpc api +- migrate event handling to jsonrpc api - Update translations (22.09.2022) - click on selected chat in chatlist now goes to bottom or first unread message - remember last path in "save as" dialog @@ -23,6 +25,9 @@ - make contact last seen always display relative time - hide ephemeral timer menu options for mailing lists #2920 - reposition ConnectivityToast +- only show core events in frontend dev console if deltachat was started with `--log-debug` or `--devmode` +- always show sticker tab now and add a button to quickly open the sticker folder. +- update deltachat-node and deltachat/jsonrpc-client to v1.97.0 ## Fixed - allow scanning of certain qr code types on welcome screen (account, url and text) @@ -32,6 +37,8 @@ - fix quote linebreaks #2870 - fix low resolution of copy qrcode image #2907 - fix group join qr code when creating a new group +- message search: show "1000+ messages", because 1000 as result means the result was truncated most of the time +- fix contact name is not updated in view profile #2945 ## [1.32.1] - 2022-08-18 diff --git a/_locales/_untranslated_en.json b/_locales/_untranslated_en.json index 3ded16a583..750a25a93a 100644 --- a/_locales/_untranslated_en.json +++ b/_locales/_untranslated_en.json @@ -11,19 +11,19 @@ "what_is_webxdc": { "message": "What is Webxdc?" }, - "file_menu" : { + "file_menu": { "message": "File" }, - "create_broadcast_list" : { + "create_broadcast_list": { "message": "Create Broadcast List" }, - "menu_edit_broadcast_list" : { + "menu_edit_broadcast_list": { "message": "Edit Broadcast List" }, - "menu_broadcast_list_name" : { + "menu_broadcast_list_name": { "message": "Broadcast List Name" }, - "broadcast_please_enter_broadcast_list_name" : { + "broadcast_please_enter_broadcast_list_name": { "message": "Please enter a name for the broadcast list." }, "broadcast_list_warning": { @@ -35,7 +35,13 @@ "clear_chat": { "message": "Clear Chat" }, - "last_seen":{ + "last_seen": { "message": "Last seen %1$s" + }, + "search_in_chat": { + "message": "Search in Chat" + }, + "exit_search": { + "message": "Exit Search" } } diff --git a/docs/LOGGING.md b/docs/LOGGING.md index 430c811ba0..7c47a840a4 100644 --- a/docs/LOGGING.md +++ b/docs/LOGGING.md @@ -39,3 +39,16 @@ Basically the log files are **tab separated** `CSV`-files(also known as `TSV`): | timestamp | location / module | level | stacktrace | arg1 | arg2 | ... | | -------------------------- | ----------------- | ------ | ---------- | ------------- | ---- | --- | | "2019-01-27T13:46:31.801Z" | "main/deltachat" | "INFO" | \[] | "dc_get_info" | - | ... | + +#### Tips and Tricks for working with the browser console: + +##### Use the search to filter the output like: + +space seperate terms, exclude with -, if your term contains spaces you should exape it with " + +`-👻` - don't show events from background accounts (not selected accounts) +`-📡` - don't show any events +`-renderer/jsonrpc` - don't show jsonrpc messages +`renderer/jsonrpc` - show only jsonrpc messages + +Start deltachat with --devmode (or --log-debug and --log-to-console) argument to show full log output. diff --git a/package-lock.json b/package-lock.json index e270acebff..9ea99490f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,14 +11,14 @@ "license": "GPL-3.0-or-later", "dependencies": { "@blueprintjs/core": "^4.1.2", - "@deltachat/jsonrpc-client": "^1.96.0", + "@deltachat/jsonrpc-client": "^1.97.0", "@deltachat/message_parser_wasm": "^0.4.0", "@deltachat/react-qr-reader": "^4.0.0", "@mapbox/geojson-extent": "^1.0.0", "application-config": "^1.0.1", "classnames": "^2.3.1", "debounce": "^1.2.0", - "deltachat-node": "^1.96.0", + "deltachat-node": "^1.97.0", "emoji-js-clean": "^4.0.0", "emoji-mart": "^3.0.1", "emoji-regex": "^9.2.2", @@ -1971,9 +1971,9 @@ } }, "node_modules/@deltachat/jsonrpc-client": { - "version": "1.96.0", - "resolved": "https://registry.npmjs.org/@deltachat/jsonrpc-client/-/jsonrpc-client-1.96.0.tgz", - "integrity": "sha512-MyiqYkMUZzEbwMIxB7Q49g10wO4DB9eTjGfeyCiZECfVq8wcTFRGiIhw92EIHobDSSJ7YBKAHksdLWBxSao56A==", + "version": "1.97.0", + "resolved": "https://registry.npmjs.org/@deltachat/jsonrpc-client/-/jsonrpc-client-1.97.0.tgz", + "integrity": "sha512-LHF1ugH7G+wO+/Y2wU01r+ugPyF2uDl+uzK+CbPj5fB3MbKjyPhGM7aydzZLTeJj70PbA5lE7TcrE6nsElA0Rw==", "dependencies": { "isomorphic-ws": "^4.0.1", "tiny-emitter": "git+https://github.com/Simon-Laux/tiny-emitter.git", @@ -5375,9 +5375,9 @@ } }, "node_modules/deltachat-node": { - "version": "1.96.0", - "resolved": "https://registry.npmjs.org/deltachat-node/-/deltachat-node-1.96.0.tgz", - "integrity": "sha512-YABeKtP9kdyQaejfuzdNkD2i4xuw1Ux9PGk8jiH96u99VwF0QumapjOSDTyn9f5tlZIYlp8MN+FGnhwvVYDonw==", + "version": "1.97.0", + "resolved": "https://registry.npmjs.org/deltachat-node/-/deltachat-node-1.97.0.tgz", + "integrity": "sha512-wd/jxNijzyt7vbV8DF1xfHUHBbsuKNm8CuN2ZuHmEiKiM3YFwCj+BCqcYZaHtVp7JTCe5naR5oFOMArWZrF1ow==", "hasInstallScript": true, "dependencies": { "debug": "^4.1.1", @@ -19107,9 +19107,9 @@ } }, "@deltachat/jsonrpc-client": { - "version": "1.96.0", - "resolved": "https://registry.npmjs.org/@deltachat/jsonrpc-client/-/jsonrpc-client-1.96.0.tgz", - "integrity": "sha512-MyiqYkMUZzEbwMIxB7Q49g10wO4DB9eTjGfeyCiZECfVq8wcTFRGiIhw92EIHobDSSJ7YBKAHksdLWBxSao56A==", + "version": "1.97.0", + "resolved": "https://registry.npmjs.org/@deltachat/jsonrpc-client/-/jsonrpc-client-1.97.0.tgz", + "integrity": "sha512-LHF1ugH7G+wO+/Y2wU01r+ugPyF2uDl+uzK+CbPj5fB3MbKjyPhGM7aydzZLTeJj70PbA5lE7TcrE6nsElA0Rw==", "requires": { "isomorphic-ws": "^4.0.1", "tiny-emitter": "git+https://github.com/Simon-Laux/tiny-emitter.git", @@ -21786,9 +21786,9 @@ "dev": true }, "deltachat-node": { - "version": "1.96.0", - "resolved": "https://registry.npmjs.org/deltachat-node/-/deltachat-node-1.96.0.tgz", - "integrity": "sha512-YABeKtP9kdyQaejfuzdNkD2i4xuw1Ux9PGk8jiH96u99VwF0QumapjOSDTyn9f5tlZIYlp8MN+FGnhwvVYDonw==", + "version": "1.97.0", + "resolved": "https://registry.npmjs.org/deltachat-node/-/deltachat-node-1.97.0.tgz", + "integrity": "sha512-wd/jxNijzyt7vbV8DF1xfHUHBbsuKNm8CuN2ZuHmEiKiM3YFwCj+BCqcYZaHtVp7JTCe5naR5oFOMArWZrF1ow==", "requires": { "debug": "^4.1.1", "napi-macros": "^2.0.0", diff --git a/package.json b/package.json index 9cf697c081..dded61a75c 100644 --- a/package.json +++ b/package.json @@ -78,14 +78,14 @@ }, "dependencies": { "@blueprintjs/core": "^4.1.2", - "@deltachat/jsonrpc-client": "^1.96.0", + "@deltachat/jsonrpc-client": "^1.97.0", "@deltachat/message_parser_wasm": "^0.4.0", "@deltachat/react-qr-reader": "^4.0.0", "@mapbox/geojson-extent": "^1.0.0", "application-config": "^1.0.1", "classnames": "^2.3.1", "debounce": "^1.2.0", - "deltachat-node": "^1.96.0", + "deltachat-node": "^1.97.0", "emoji-js-clean": "^4.0.0", "emoji-mart": "^3.0.1", "emoji-regex": "^9.2.2", diff --git a/scss/chat/_chat-list.scss b/scss/chat/_chat-list.scss index 7dc01e8a29..5bd56e4dcb 100644 --- a/scss/chat/_chat-list.scss +++ b/scss/chat/_chat-list.scss @@ -18,4 +18,25 @@ border-bottom: var(--cli-search-result-divider-border-width) solid var(--cli-search-result-divider-border); } + + .search-in-chat-label { + display: flex; + height: 64px; + flex-direction: row; + padding: 0px 10px; + align-items: center; + + .chat-name { + flex-grow: 1; + vertical-align: baseline; + line-height: 18px; + overflow-x: hidden; + overflow-y: hidden; + white-space: nowrap; + text-overflow: ellipsis; + font-weight: 200; + font-size: medium; + margin-left: 10px; + } + } } diff --git a/src/main/deltachat/backup.ts b/src/main/deltachat/backup.ts deleted file mode 100644 index 63c627afb5..0000000000 --- a/src/main/deltachat/backup.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { C } from 'deltachat-node' - -import { getLogger } from '../../shared/logger' -const log = getLogger('main/deltachat/backup') - -import SplitOut from './splitout' -import { DeltaChatAccount } from '../../shared/shared-types' -export default class DCBackup extends SplitOut { - async export(dir: string) { - this.selectedAccountContext.stopIO() - try { - await this._internal_export(dir) - } catch (err) { - this.selectedAccountContext.startIO() - throw err - } - this.selectedAccountContext.startIO() - } - - private async _internal_export(dir: string) { - return new Promise((resolve, reject) => { - this.selectedAccountContext.importExport( - C.DC_IMEX_EXPORT_BACKUP, - dir, - undefined - ) - const onEventImexProgress = ( - _accountId: number, - data1: number, - _data2: number - ) => { - if (data1 === 0) { - this.accounts.removeListener( - 'DC_EVENT_IMEX_PROGRESS', - onEventImexProgress - ) - reject('Backup export failed (progress==0)') - } else if (data1 === 1000) { - this.accounts.removeListener( - 'DC_EVENT_IMEX_PROGRESS', - onEventImexProgress - ) - resolve() - } - } - - this.accounts.on('DC_EVENT_IMEX_PROGRESS', onEventImexProgress) - }) - } - - import(file: string): Promise { - if (!this.selectedAccountId || !this.selectedAccountContext) { - throw new Error('Import of backup needs selected context.') - } - const accountId = this.selectedAccountId - const dcnContext = this.selectedAccountContext - - return new Promise((resolve, reject) => { - const onFail = (reason: String) => { - reject(reason) - } - - const onSuccess = () => { - resolve(this.controller.login._accountInfo(accountId)) - } - - this.accounts.on( - 'DC_EVENT_IMEX_PROGRESS', - async (eventAccountId, data1, data2) => { - if (eventAccountId !== accountId) return - if (data1 === 0) { - onFail(data2) - } else if (data1 === 1000) { - onSuccess() - } - } - ) - - log.debug(`openend context`) - log.debug(`Starting backup import of ${file}`) - - dcnContext.importExport(C.DC_IMEX_IMPORT_BACKUP, file, '') - }) - } -} diff --git a/src/main/deltachat/chat.ts b/src/main/deltachat/chat.ts deleted file mode 100644 index ae34ed784a..0000000000 --- a/src/main/deltachat/chat.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { C } from 'deltachat-node' -import { getLogger } from '../../shared/logger' -import SplitOut from './splitout' - -const log = getLogger('main/deltachat/chat') -export default class DCChat extends SplitOut { - leaveGroup(chatId: number) { - log.debug(`action - leaving chat ${chatId}`) - this.selectedAccountContext.removeContactFromChat( - chatId, - C.DC_CONTACT_ID_SELF - ) - } - - setName(chatId: number, name: string) { - return this.selectedAccountContext.setChatName(chatId, name) - } - - modifyGroup( - chatId: number, - name: string, - image: string | undefined, - members: number[] | null - ) { - log.debug('action - modify group', { chatId, name, image, members }) - this.selectedAccountContext.setChatName(chatId, name) - const chat = this.selectedAccountContext.getChat(chatId) - if (!chat) { - throw new Error('chat is undefined, this should not happen') - } - if (typeof image !== 'undefined' && chat.getProfileImage() !== image) { - this.selectedAccountContext.setChatProfileImage(chatId, image || '') - } - - if (members !== null) { - const previousMembers = [ - ...this.selectedAccountContext.getChatContacts(chatId), - ] - const remove = previousMembers.filter(m => !members.includes(m)) - const add = members.filter(m => !previousMembers.includes(m)) - - remove.forEach(id => - this.selectedAccountContext.removeContactFromChat(chatId, id) - ) - add.forEach(id => - this.selectedAccountContext.addContactToChat(chatId, id) - ) - } - return true - } - - addContactToChat(chatId: number, contactId: number) { - return this.selectedAccountContext.addContactToChat(chatId, contactId) - } - - setProfileImage(chatId: number, newImage: string) { - return this.selectedAccountContext.setChatProfileImage(chatId, newImage) - } - - /** - * @returns id of the created chat - */ - createGroupChat(verified: boolean, name: string) { - return this.selectedAccountContext.createGroupChat(name, verified) - } - - /** - * @returns id of the created chat - */ - createBroadcastList() { - return this.selectedAccountContext.createBroadcastList() - } - - setVisibility( - chatId: number, - visibility: - | C.DC_CHAT_VISIBILITY_NORMAL - | C.DC_CHAT_VISIBILITY_ARCHIVED - | C.DC_CHAT_VISIBILITY_PINNED - ) { - log.debug(`action - set chat ${chatId} visibility ${visibility}`) - this.selectedAccountContext.setChatVisibility(chatId, visibility) - } - - getChatContacts(chatId: number) { - return this.selectedAccountContext.getChatContacts(chatId) - } - - getChatEphemeralTimer(chatId: number) { - return this.selectedAccountContext.getChatEphemeralTimer(chatId) - } - - setChatEphemeralTimer(chatId: number, timer: number) { - return this.selectedAccountContext.setChatEphemeralTimer(chatId, timer) - } - - async sendVideoChatInvitation(chatId: number) { - return await this.selectedAccountContext.sendVideochatInvitation(chatId) - } -} diff --git a/src/main/deltachat/contacts.ts b/src/main/deltachat/contacts.ts deleted file mode 100644 index f3effe5e0d..0000000000 --- a/src/main/deltachat/contacts.ts +++ /dev/null @@ -1,26 +0,0 @@ -import DeltaChat from 'deltachat-node' - -import SplitOut from './splitout' - -export default class JsonContacts extends SplitOut { - async changeNickname(contactId: number, name: string) { - const contact = this.selectedAccountContext.getContact(contactId) - if (!contact) { - throw new Error('contact not found') - } - const address = contact.getAddress() - const result = this.selectedAccountContext.createContact(name, address) - return result - } - - lookupContactIdByAddr(email: string): number { - if (!DeltaChat.maybeValidAddr(email)) { - throw new Error(this.controller.translate('bad_email_address')) - } - return this.selectedAccountContext.lookupContactIdByAddr(email) - } - - deleteContact(contactId: number) { - return this.selectedAccountContext.deleteContact(contactId) - } -} diff --git a/src/main/deltachat/controller.ts b/src/main/deltachat/controller.ts index 9aee6e9809..49cd188a40 100644 --- a/src/main/deltachat/controller.ts +++ b/src/main/deltachat/controller.ts @@ -3,20 +3,9 @@ import { app as rawApp, ipcMain } from 'electron' import { EventEmitter } from 'events' import { getLogger } from '../../shared/logger' import * as mainWindow from '../windows/main' -import DCBackup from './backup' -import DCChat from './chat' -import JsonContacts from './contacts' import DCContext from './context' -import DCLocations from './locations' import DCLoginController from './login' -import DCMessageList from './messagelist' -import DCSettings from './settings' -import DCStickers from './stickers' import { ExtendedAppMainProcess } from '../types' -import Extras from './extras' - -import { VERSION, BUILD_TIMESTAMP } from '../../shared/build-info' -import { Timespans, DAYS_UNTIL_UPDATE_SUGGESTION } from '../../shared/constants' import { Context } from 'deltachat-node/node/dist/context' import path, { join } from 'path' import { existsSync, lstatSync } from 'fs' @@ -31,6 +20,7 @@ import { tx } from '../load-translations' const app = rawApp as ExtendedAppMainProcess const log = getLogger('main/deltachat') const logCoreEvent = getLogger('core/event') +const logCoreEventM = getLogger('core/event/m') const logMigrate = getLogger('main/migrate') /** @@ -68,12 +58,7 @@ export default class DeltaChatController extends EventEmitter { ready = false // used for the about screen constructor(public cwd: string) { super() - this.onAll = this.onAll.bind(this) - this.onChatlistUpdated = this.onChatlistUpdated.bind(this) - this.onMsgsChanged = this.onMsgsChanged.bind(this) - this.onIncomingMsg = this.onIncomingMsg.bind(this) - this.onChatModified = this.onChatModified.bind(this) } async init() { @@ -81,23 +66,38 @@ export default class DeltaChatController extends EventEmitter { await this.migrateToAccountsApiIfNeeded() - setInterval( - // If the dc is always on - this.hintUpdateIfNessesary.bind(this), - Timespans.ONE_DAY_IN_SECONDS * 1000 - ) - log.debug('Initiating DeltaChatNode') this._inner_account_manager = new DeltaChatNode( this.cwd, 'deltachat-desktop' ) - log.debug('Starting event handler') - this.registerEventHandler(this.account_manager) - this.account_manager.startJsonRpcHandler(response => { mainWindow.send('json-rpc-message', response) + if (response.indexOf('event') !== -1) + try { + const { method, params } = JSON.parse(response) + if (method === 'event') { + const { contextId, event } = params + if (event.type === 'Warning') { + logCoreEvent.warn(contextId, event.msg) + } else if (event === 'Info') { + logCoreEvent.info(contextId, event.msg) + } else if (event.startsWith('Error')) { + logCoreEvent.error(contextId, event.msg) + } else if (app.rc['log-debug']) { + // in debug mode log all core events + const event_clone = Object.assign({}, event) as Partial< + typeof event + > + delete event_clone.type + logCoreEvent.debug(event.type, contextId, event) + } + } + } catch (error) { + // ignore json parse errors + return + } }) ipcMain.handle('json-rpc-request', (_ev, message) => { @@ -228,6 +228,7 @@ export default class DeltaChatController extends EventEmitter { } } + this.unregisterEventHandler(tmp_dc) tmp_dc.close() // Clear some settings that we cant migrate DesktopSettings.update({ @@ -247,16 +248,8 @@ export default class DeltaChatController extends EventEmitter { logMigrate.info('migration completed') } - readonly backup = new DCBackup(this) - readonly contacts = new JsonContacts(this) - readonly chat = new DCChat(this) - readonly locations = new DCLocations(this) readonly login = new DCLoginController(this) - readonly messageList = new DCMessageList(this) - readonly settings = new DCSettings(this) - readonly stickers = new DCStickers(this) readonly context = new DCContext(this) - readonly extras = new Extras(this) readonly webxdc = new DCWebxdc(this) /** @@ -341,88 +334,24 @@ export default class DeltaChatController extends EventEmitter { onAll(event: string, accountId: number, data1: any, data2: any) { if (event === 'DC_EVENT_WARNING') { - logCoreEvent.warn(accountId, event, data1, data2) + logCoreEventM.warn(accountId, event, data1, data2) } else if (event === 'DC_EVENT_INFO') { - logCoreEvent.info(accountId, event, data1, data2) + logCoreEventM.info(accountId, event, data1, data2) } else if (event.startsWith('DC_EVENT_ERROR')) { - logCoreEvent.error(accountId, event, data1, data2) + logCoreEventM.error(accountId, event, data1, data2) } else if (app.rc['log-debug']) { // in debug mode log all core events - logCoreEvent.debug(accountId, event, data1, data2) + logCoreEventM.debug(accountId, event, data1, data2) } - - this.emit('ALL', event, accountId, data1, data2) - this.emit(event, accountId, data1, data2) - - if (accountId === this.selectedAccountId) { - this.sendToRenderer(event, [data1, data2]) - } - } - - onMsgsChanged(accountId: number, _chatId: number, _msgId: number) { - if (this.selectedAccountId !== accountId) { - return - } - this.onChatlistUpdated() - } - - onIncomingMsg(accountId: number, _chatId: number, _msgId: number) { - // TODO better do proper event sorting in the frontend so we can listen there for this event - this.sendToRenderer('DD_EVENT_INCOMING_MESSAGE_ACCOUNT', accountId) - if (this.selectedAccountId !== accountId) { - return - } - this.onChatlistUpdated() - } - - onChatModified(accountId: number, _chatId: number, _msgId: number) { - if (this.selectedAccountId !== accountId) { - return - } - this.onChatlistUpdated() } registerEventHandler(dc: DeltaChat) { dc.startEvents() dc.on('ALL', this.onAll.bind(this)) - dc.on('DD_EVENT_CHATLIST_UPDATED', this.onChatlistUpdated) - dc.on('DC_EVENT_MSGS_CHANGED', this.onMsgsChanged) - dc.on('DC_EVENT_INCOMING_MSG', this.onIncomingMsg) - dc.on('DC_EVENT_CHAT_MODIFIED', this.onChatModified) } unregisterEventHandler(dc: DeltaChat) { dc.removeListener('ALL', this.onAll) - dc.removeListener('DD_EVENT_CHATLIST_UPDATED', this.onChatlistUpdated) - dc.removeListener('DC_EVENT_MSGS_CHANGED', this.onMsgsChanged) - dc.removeListener('DC_EVENT_INCOMING_MSG', this.onIncomingMsg) - dc.removeListener('DC_EVENT_CHAT_MODIFIED', this.onChatModified) - } - - onChatlistUpdated() { - this.sendToRenderer('DD_EVENT_CHATLIST_CHANGED', {}) - } - - joinSecurejoin(qrCode: string) { - return this.selectedAccountContext.joinSecurejoin(qrCode) - } - - setProfilePicture(newImage: string) { - this.selectedAccountContext.setConfig('selfavatar', newImage) - } - - hintUpdateIfNessesary() { - if ( - this.selectedAccountContext && - Date.now() > - Timespans.ONE_DAY_IN_SECONDS * DAYS_UNTIL_UPDATE_SUGGESTION * 1000 + - BUILD_TIMESTAMP - ) { - this.selectedAccountContext.addDeviceMessage( - `update-suggestion-${VERSION}`, - `This build is over ${DAYS_UNTIL_UPDATE_SUGGESTION} days old - There might be a new version available. -> https://get.delta.chat` - ) - } } /** diff --git a/src/main/deltachat/extras.ts b/src/main/deltachat/extras.ts deleted file mode 100644 index 07276ca65e..0000000000 --- a/src/main/deltachat/extras.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { app as rawApp, clipboard } from 'electron' -import { getLogger } from '../../shared/logger' -import SplitOut from './splitout' -import { LocaleData } from '../../shared/localize' -import setLanguage, { - getCurrentLocaleDate, - loadTranslations, -} from '../load-translations' -import { loadTheme, getAvailableThemes, resolveThemeAddress } from '../themes' -import { txCoreStrings } from './login' -const log = getLogger('main/deltachat/extras') -import { refresh as refreshMenu } from '../menu' -import { join } from 'path' -import mimeTypes from 'mime-types' -import { writeFile } from 'fs/promises' -import { DesktopSettings } from '../desktop_settings' - -// Extras, mainly Electron functions -export default class Extras extends SplitOut { - getLocaleData(locale: string): LocaleData { - if (locale) { - loadTranslations(locale) - } - return getCurrentLocaleDate() - } - setLocale(locale: string) { - setLanguage(locale) - this.controller.login._setCoreStrings(txCoreStrings()) - refreshMenu() - } - async getActiveTheme() { - try { - log.debug('theme', DesktopSettings.state.activeTheme) - return await loadTheme(DesktopSettings.state.activeTheme) - } catch (error) { - log.error('loading theme failed:', error) - return null - } - } - async setTheme(address: string) { - try { - resolveThemeAddress(address) - DesktopSettings.update({ activeTheme: address }) - DesktopSettings.update({}) - return true - } catch (error) { - log.error('set theme failed: ', error) - return false - } - } - - async getAvailableThemes() { - return await getAvailableThemes() - } - - async writeClipboardToTempFile() { - const formats = clipboard.availableFormats().sort() - log.debug('Clipboard available formats:', formats) - if (formats.length <= 0) { - throw new Error('No files to write') - } - const pathToFile = join( - rawApp.getPath('temp'), - `paste.${mimeTypes.extension(formats[0]) || 'bin'}` - ) - const buf = - mimeTypes.extension(formats[0]) === 'png' - ? clipboard.readImage().toPNG() - : clipboard.readBuffer(formats[0]) - log.debug(`Writing clipboard ${formats[0]} to file ${pathToFile}`) - await writeFile(pathToFile, buf, 'binary') - return pathToFile - } -} diff --git a/src/main/deltachat/locations.ts b/src/main/deltachat/locations.ts deleted file mode 100644 index e08b27bc8b..0000000000 --- a/src/main/deltachat/locations.ts +++ /dev/null @@ -1,24 +0,0 @@ -import SplitOut from './splitout' -export default class DCLocations extends SplitOut { - setLocation(latitude: number, longitude: number, accuracy: number) { - return this.selectedAccountContext.setLocation( - latitude, - longitude, - accuracy - ) - } - - getLocations( - chatId: number, - contactId: number, - timestampFrom: number, - timestampTo: number - ) { - return this.selectedAccountContext.getLocations( - chatId, - contactId, - timestampFrom, - timestampTo - ) - } -} diff --git a/src/main/deltachat/login.ts b/src/main/deltachat/login.ts index 558b54806e..ca5f9194f2 100644 --- a/src/main/deltachat/login.ts +++ b/src/main/deltachat/login.ts @@ -1,36 +1,10 @@ -import { C } from 'deltachat-node' import { getLogger } from '../../shared/logger' import '../notifications' import SplitOut from './splitout' -import { DeltaChatAccount } from '../../shared/shared-types' -import { stat, readdir } from 'fs/promises' -import { join } from 'path' -import { Context } from 'deltachat-node/node/dist/context' import { DesktopSettings } from '../desktop_settings' -import { tx } from '../load-translations' const log = getLogger('main/deltachat/login') -function setCoreStrings( - dc: Readonly, - strings: { [key: number]: string } -) { - Object.keys(strings).forEach(key => { - dc.setStockTranslation(Number(key), strings[Number(key)]) - }) -} - export default class DCLoginController extends SplitOut { - /** - * Called when this controller is created and when current - * locale changes - */ - _setCoreStrings(strings: { [key: number]: string }) { - if (!this.controller._inner_selectedAccountContext) { - return - } - setCoreStrings(this.controller.selectedAccountContext, strings) - } - async selectAccount(accountId: number) { log.debug('selectAccount', accountId) this.controller.selectedAccountId = accountId @@ -40,8 +14,6 @@ export default class DCLoginController extends SplitOut { this.controller._inner_selectedAccountContext = this.accounts.accountContext( accountId ) - log.debug('Set core translations') - this.controller.login._setCoreStrings(txCoreStrings()) log.info('Ready, starting io...') this.controller.selectedAccountContext.startIO() @@ -52,18 +24,11 @@ export default class DCLoginController extends SplitOut { log.info('dc_get_info', this.selectedAccountContext.getInfo()) - this.updateDeviceChats() - this.controller.ready = true return true } logout() { - DesktopSettings.update({ lastAccount: undefined }) - - if (!DesktopSettings.state.syncAllAccounts) { - this.selectedAccountContext.stopIO() - } if (this.controller._inner_selectedAccountContext) { this.controller.selectedAccountContext.unref() } @@ -81,278 +46,4 @@ export default class DCLoginController extends SplitOut { this.accounts.close() this.controller._inner_account_manager = null } - - updateDeviceChats() { - this.controller.hintUpdateIfNessesary() - - this.selectedAccountContext.addDeviceMessage( - 'changelog-version-1.32.0-version0', - `What's new in 1.32.0? -2️⃣ New experimental features: Broadcast lists and Automated Email Address Porting -➕ Floating action button in chatlist to start a new chat -🚆 Many reliability improvements and bugfixes - -Full changelog: https://github.com/deltachat/deltachat-desktop/blob/master/CHANGELOG.md#1310---2022-07-17` - ) - } - - async _accountInfo(accountId: number): Promise { - const accountContext = this.accounts.accountContext(accountId) - - if (accountContext.isConfigured()) { - const selfContact = accountContext.getContact(C.DC_CONTACT_ID_SELF) - if (!selfContact) { - log.error('selfContact is undefined') - } - const [display_name, addr, profile_image, color] = [ - accountContext.getConfig('displayname'), - accountContext.getConfig('addr'), - selfContact?.getProfileImage() || '', - selfContact?.color || 'red', - ] - accountContext.unref() - return { - type: 'configured', - id: accountId, - display_name, - addr, - profile_image, - color, - } - } else { - accountContext.unref() - return { - type: 'unconfigured', - id: accountId, - } - } - } - - async getAccountSize(accountId: number): Promise { - const accountContext = this.accounts.accountContext(accountId) - const account_dir = join(accountContext.getBlobdir(), '..') - accountContext.unref() - return await _getAccountSize(account_dir) - } - - async getLastLoggedInAccount() { - if (DesktopSettings.state.lastAccount) { - try { - this.controller.account_manager.accountContext( - DesktopSettings.state.lastAccount - ) - return DesktopSettings.state.lastAccount - } catch (error) { - log.warn( - `account with id ${DesktopSettings.state.lastAccount} does not exist` - ) - } - } - return undefined - } -} - -export function txCoreStrings() { - const strings: { [key: number]: string } = {} - // TODO: Check if we need the uncommented core translations - strings[C.DC_STR_NOMESSAGES] = tx('chat_no_messages') - strings[C.DC_STR_SELF] = tx('self') - strings[C.DC_STR_DRAFT] = tx('draft') - strings[C.DC_STR_VOICEMESSAGE] = tx('voice_message') - strings[C.DC_STR_IMAGE] = tx('image') - strings[C.DC_STR_GIF] = tx('gif') - strings[C.DC_STR_VIDEO] = tx('video') - strings[C.DC_STR_AUDIO] = tx('audio') - strings[C.DC_STR_FILE] = tx('file') - strings[C.DC_STR_ENCRYPTEDMSG] = tx('encrypted_message') - // strings[C.DC_STR_E2E_AVAILABLE] = tx('DC_STR_E2E_AVAILABLE') - // strings[C.DC_STR_ENCR_TRANSP] = tx('DC_STR_ENCR_TRANSP') - // strings[C.DC_STR_ENCR_NONE] = tx('DC_STR_ENCR_NONE') - strings[C.DC_STR_FINGERPRINTS] = tx('qrscan_fingerprint_label') - strings[C.DC_STR_CANTDECRYPT_MSG_BODY] = tx('systemmsg_cannot_decrypt') - strings[C.DC_STR_READRCPT] = tx('systemmsg_read_receipt_subject') - strings[C.DC_STR_READRCPT_MAILBODY] = tx('systemmsg_read_receipt_body') - strings[C.DC_STR_E2E_PREFERRED] = tx('autocrypt_prefer_e2ee') - strings[C.DC_STR_ARCHIVEDCHATS] = tx('chat_archived_chats_title') - strings[C.DC_STR_AC_SETUP_MSG_SUBJECT] = tx('autocrypt_asm_subject') - strings[C.DC_STR_AC_SETUP_MSG_BODY] = tx('autocrypt_asm_general_body') - strings[C.DC_STR_CANNOT_LOGIN] = tx('login_error_cannot_login') - strings[C.DC_STR_DEVICE_MESSAGES] = tx('device_talk') - strings[C.DC_STR_SAVED_MESSAGES] = tx('saved_messages') - strings[C.DC_STR_CONTACT_VERIFIED] = tx('contact_verified') - strings[C.DC_STR_CONTACT_NOT_VERIFIED] = tx('contact_not_verified') - strings[C.DC_STR_CONTACT_SETUP_CHANGED] = tx('contact_setup_changed') - strings[C.DC_STR_DEVICE_MESSAGES_HINT] = tx('device_talk_explain') - strings[C.DC_STR_WELCOME_MESSAGE] = tx('device_talk_welcome_message') - strings[C.DC_STR_UNKNOWN_SENDER_FOR_CHAT] = tx( - 'systemmsg_unknown_sender_for_chat' - ) - strings[C.DC_STR_SUBJECT_FOR_NEW_CONTACT] = tx( - 'systemmsg_subject_for_new_contact' - ) - strings[C.DC_STR_FAILED_SENDING_TO] = tx('systemmsg_failed_sending_to') - strings[C.DC_STR_VIDEOCHAT_INVITATION] = tx('videochat_invitation') - strings[C.DC_STR_VIDEOCHAT_INVITE_MSG_BODY] = tx('videochat_invitation_body') - strings[C.DC_STR_CONFIGURATION_FAILED] = tx('configuration_failed_with_error') - strings[C.DC_STR_REPLY_NOUN] = tx('reply_noun') - strings[C.DC_STR_FORWARDED] = tx('forwarded') - - //strings[C.DC_STR_MSGLOCATIONENABLED] = tx('') - //strings[C.DC_STR_MSGLOCATIONDISABLED] = tx('') - strings[C.DC_STR_LOCATION] = tx('location') - strings[C.DC_STR_STICKER] = tx('sticker') - strings[C.DC_STR_BAD_TIME_MSG_BODY] = tx('devicemsg_bad_time') - strings[C.DC_STR_UPDATE_REMINDER_MSG_BODY] = tx('devicemsg_update_reminder') - //strings[C.DC_STR_ERROR_NO_NETWORK] = tx('') - strings[C.DC_STR_SELF_DELETED_MSG_BODY] = tx('devicemsg_self_deleted') - //strings[C.DC_STR_SERVER_TURNED_OFF] = tx('') - strings[C.DC_STR_QUOTA_EXCEEDING_MSG_BODY] = tx('devicemsg_storage_exceeding') - strings[C.DC_STR_PARTIAL_DOWNLOAD_MSG_BODY] = tx('n_bytes_message') - strings[C.DC_STR_DOWNLOAD_AVAILABILITY] = tx('download_max_available_until') - //strings[C.DC_STR_SYNC_MSG_SUBJECT] = tx('') - //strings[C.DC_STR_SYNC_MSG_BODY] = tx('') - strings[C.DC_STR_INCOMING_MESSAGES] = tx('incoming_messages') - strings[C.DC_STR_OUTGOING_MESSAGES] = tx('outgoing_messages') - strings[C.DC_STR_STORAGE_ON_DOMAIN] = tx('storage_on_domain') - strings[C.DC_STR_ONE_MOMENT] = tx('one_moment') - strings[C.DC_STR_CONNECTED] = tx('connectivity_connected') - strings[C.DC_STR_CONNTECTING] = tx('connectivity_connecting') - strings[C.DC_STR_UPDATING] = tx('connectivity_updating') - strings[C.DC_STR_SENDING] = tx('sending') - strings[C.DC_STR_LAST_MSG_SENT_SUCCESSFULLY] = tx( - 'last_msg_sent_successfully' - ) - strings[C.DC_STR_ERROR] = tx('error_x') - strings[C.DC_STR_NOT_SUPPORTED_BY_PROVIDER] = tx('not_supported_by_provider') - strings[C.DC_STR_MESSAGES] = tx('messages') - strings[C.DC_STR_BROADCAST_LIST] = tx('broadcast_list') - strings[C.DC_STR_PART_OF_TOTAL_USED] = tx('part_of_total_used') - strings[C.DC_STR_SECURE_JOIN_STARTED] = tx('secure_join_started') - strings[C.DC_STR_SECURE_JOIN_REPLIES] = tx('secure_join_replies') - strings[C.DC_STR_SETUP_CONTACT_QR_DESC] = tx('qrshow_join_contact_hint') - strings[C.DC_STR_SECURE_JOIN_GROUP_QR_DESC] = tx('qrshow_join_group_hint') - strings[C.DC_STR_NOT_CONNECTED] = tx('connectivity_not_connected') - strings[C.DC_STR_AEAP_ADDR_CHANGED] = tx('aeap_addr_changed') - strings[C.DC_STR_AEAP_EXPLANATION_AND_LINK] = tx('aeap_explanation') - - strings[C.DC_STR_GROUP_NAME_CHANGED_BY_YOU] = tx('group_name_changed_by_you') - strings[C.DC_STR_GROUP_NAME_CHANGED_BY_OTHER] = tx( - 'group_name_changed_by_other' - ) - strings[C.DC_STR_GROUP_IMAGE_CHANGED_BY_YOU] = tx( - 'group_image_changed_by_you' - ) - strings[C.DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER] = tx( - 'group_image_changed_by_other' - ) - strings[C.DC_STR_ADD_MEMBER_BY_YOU] = tx('add_member_by_you') - strings[C.DC_STR_ADD_MEMBER_BY_OTHER] = tx('add_member_by_other') - strings[C.DC_STR_REMOVE_MEMBER_BY_YOU] = tx('remove_member_by_you') - strings[C.DC_STR_REMOVE_MEMBER_BY_OTHER] = tx('remove_member_by_other') - strings[C.DC_STR_GROUP_LEFT_BY_YOU] = tx('group_left_by_you') - strings[C.DC_STR_GROUP_LEFT_BY_OTHER] = tx('group_left_by_other') - strings[C.DC_STR_GROUP_IMAGE_DELETED_BY_YOU] = tx( - 'group_image_deleted_by_you' - ) - strings[C.DC_STR_GROUP_IMAGE_DELETED_BY_OTHER] = tx( - 'group_image_deleted_by_other' - ) - strings[C.DC_STR_LOCATION_ENABLED_BY_YOU] = tx('location_enabled_by_you') - strings[C.DC_STR_LOCATION_ENABLED_BY_OTHER] = tx('location_enabled_by_other') - strings[C.DC_STR_EPHEMERAL_TIMER_DISABLED_BY_YOU] = tx( - 'ephemeral_timer_disabled_by_you' - ) - strings[C.DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER] = tx( - 'ephemeral_timer_disabled_by_other' - ) - strings[C.DC_STR_EPHEMERAL_TIMER_SECONDS_BY_YOU] = tx( - 'ephemeral_timer_seconds_by_you' - ) - strings[C.DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER] = tx( - 'ephemeral_timer_seconds_by_other' - ) - strings[C.DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU] = tx( - 'ephemeral_timer_1_minute_by_you' - ) - strings[C.DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER] = tx( - 'ephemeral_timer_1_minute_by_other' - ) - strings[C.DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_YOU] = tx( - 'ephemeral_timer_1_hour_by_you' - ) - strings[C.DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER] = tx( - 'ephemeral_timer_1_hour_by_other' - ) - strings[C.DC_STR_EPHEMERAL_TIMER_1_DAY_BY_YOU] = tx( - 'ephemeral_timer_1_day_by_you' - ) - strings[C.DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER] = tx( - 'ephemeral_timer_1_day_by_other' - ) - strings[C.DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_YOU] = tx( - 'ephemeral_timer_1_week_by_you' - ) - strings[C.DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER] = tx( - 'ephemeral_timer_1_week_by_other' - ) - strings[C.DC_STR_EPHEMERAL_TIMER_MINUTES_BY_YOU] = tx( - 'ephemeral_timer_minutes_by_you' - ) - strings[C.DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER] = tx( - 'ephemeral_timer_minutes_by_other' - ) - strings[C.DC_STR_EPHEMERAL_TIMER_HOURS_BY_YOU] = tx( - 'ephemeral_timer_hours_by_you' - ) - strings[C.DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER] = tx( - 'ephemeral_timer_hours_by_other' - ) - strings[C.DC_STR_EPHEMERAL_TIMER_DAYS_BY_YOU] = tx( - 'ephemeral_timer_days_by_you' - ) - strings[C.DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER] = tx( - 'ephemeral_timer_days_by_other' - ) - strings[C.DC_STR_EPHEMERAL_TIMER_WEEKS_BY_YOU] = tx( - 'ephemeral_timer_weeks_by_you' - ) - strings[C.DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER] = tx( - 'ephemeral_timer_weeks_by_other' - ) - strings[C.DC_STR_PROTECTION_ENABLED_BY_YOU] = tx('protection_enabled_by_you') - strings[C.DC_STR_PROTECTION_ENABLED_BY_OTHER] = tx( - 'protection_enabled_by_other' - ) - strings[C.DC_STR_PROTECTION_DISABLED_BY_YOU] = tx( - 'protection_disabled_by_you' - ) - strings[C.DC_STR_PROTECTION_DISABLED_BY_OTHER] = tx( - 'protection_disabled_by_other' - ) - - return strings -} - -async function _getAccountSize(path: string) { - try { - const db_size = (await stat(join(path, 'dc.db'))).size - const blob_dir = join(path, 'dc.db-blobs') - const blob_files = await readdir(blob_dir) - let blob_size = 0 - if (blob_files.length > 0) { - const blob_file_sizes = await Promise.all( - blob_files.map( - async blob_file => (await stat(join(blob_dir, blob_file))).size - ) - ) - blob_size = blob_file_sizes.reduce( - (totalSize, currentBlobSize) => totalSize + currentBlobSize - ) - } - - return db_size + blob_size - } catch (error) { - log.warn(error) - return 0 - } } diff --git a/src/main/deltachat/messagelist.ts b/src/main/deltachat/messagelist.ts deleted file mode 100644 index a9a6f8a7d1..0000000000 --- a/src/main/deltachat/messagelist.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { C } from 'deltachat-node' -import { getLogger } from '../../shared/logger' - -const log = getLogger('main/deltachat/messagelist') - -import SplitOut from './splitout' -import { MessageSearchResult } from '../../shared/shared-types' - -import { mkdtemp, writeFile } from 'fs/promises' -import { tmpdir } from 'os' -import { join } from 'path' - -export default class DCMessageList extends SplitOut { - sendSticker(chatId: number, fileStickerPath: string) { - const viewType = C.DC_MSG_STICKER - const msg = this.selectedAccountContext.messageNew(viewType) - const stickerPath = fileStickerPath.replace('file://', '') - msg.setFile(stickerPath, undefined) - this.selectedAccountContext.sendMessage(chatId, msg) - } - - downloadFullMessage(msgId: number) { - return this.selectedAccountContext.downloadFullMessage(msgId) - } - - searchMessages(query: string, chatId = 0): number[] { - return this.selectedAccountContext.searchMessages(chatId, query) - } - - private _msgId2SearchResultItem(msgId: number): MessageSearchResult | null { - const message = this.selectedAccountContext.getMessage(msgId) - if (!message) { - log.warn('search: message not found: msg_id ', msgId) - return null - } - const chat = this.selectedAccountContext.getChat(message.getChatId()) - const author = this.selectedAccountContext.getContact(message.getFromId()) - if (!chat || !author) { - log.warn('search: chat or author of message not found: msg_id ', msgId) - return null - } - return { - id: msgId, - authorProfileImage: author.getProfileImage(), - author_name: author.getDisplayName(), - author_color: author.color, - chat_name: chat.isSingle() ? null : chat.getName(), - message: message.getText(), - timestamp: message.getTimestamp(), - } - } - - msgIds2SearchResultItems(ids: number[]) { - const result: { [id: number]: MessageSearchResult } = {} - for (const id of ids) { - const item = this._msgId2SearchResultItem(id) - if (item) { - result[id] = item - } - } - return result - } - - /** @returns file path to html file */ - async saveMessageHTML2Disk(messageId: number): Promise { - const message_html_content = this.selectedAccountContext.getMessageHTML( - messageId - ) - const tmpDir = await mkdtemp(join(tmpdir(), 'deltachat-')) - const pathToFile = join(tmpDir, 'message.html') - await writeFile(pathToFile, message_html_content, { encoding: 'utf-8' }) - return pathToFile - } -} diff --git a/src/main/deltachat/settings.ts b/src/main/deltachat/settings.ts deleted file mode 100644 index 0266dd92cc..0000000000 --- a/src/main/deltachat/settings.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { C } from 'deltachat-node' -import { getLogger } from '../../shared/logger' - -const log = getLogger('main/deltachat/settings') - -import SplitOut from './splitout' -import { DesktopSettingsType } from '../../shared/shared-types' -import { copyFile, mkdir, rm } from 'fs/promises' -import { join, extname } from 'path' -import { getConfigPath } from '../application-constants' -import { updateTrayIcon } from '../tray' -import { DesktopSettings } from '../desktop_settings' - -export default class DCSettings extends SplitOut { - setDesktopSetting(key: keyof DesktopSettingsType, value: string) { - DesktopSettings.update({ [key]: value }) - - if (key === 'minimizeToTray') updateTrayIcon() - - if (key === 'syncAllAccounts') { - if (value) { - this.accounts.startIO() - } else { - this.accounts.stopIO() - } - } - - return true - } - - getDesktopSettings(): DesktopSettingsType { - return DesktopSettings.state - } - - keysImport(directory: string) { - this.selectedAccountContext.importExport( - C.DC_IMEX_IMPORT_SELF_KEYS, - directory, - undefined - ) - } - - keysExport(directory: string) { - this.selectedAccountContext.importExport( - C.DC_IMEX_EXPORT_SELF_KEYS, - directory, - undefined - ) - } - - async saveBackgroundImage(file: string, isDefaultPicture: boolean) { - const originalFilePath = !isDefaultPicture - ? file - : join(__dirname, '../../../images/backgrounds/', file) - - const bgDir = join(getConfigPath(), 'background') - await rm(bgDir, { recursive: true, force: true }) - await mkdir(bgDir, { recursive: true }) - const fileName = `background_${Date.now()}` + extname(originalFilePath) - const newPath = join(getConfigPath(), 'background', fileName) - try { - await copyFile(originalFilePath, newPath) - } catch (error) { - log.error('BG-IMG Copy Failed', error) - throw error - } - return `img: ${fileName.replace(/\\/g, '/')}` - } - - estimateAutodeleteCount(fromServer: boolean, seconds: number) { - return this.selectedAccountContext.estimateDeletionCount( - fromServer, - seconds - ) - } -} diff --git a/src/main/deltachat/stickers.ts b/src/main/deltachat/stickers.ts deleted file mode 100644 index a9fa55a2a3..0000000000 --- a/src/main/deltachat/stickers.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { lstat, readdir } from 'fs/promises' -import path from 'path' -import { getLogger } from '../../shared/logger' -import SplitOut from './splitout' - -const log = getLogger('main/deltachat/stickers') - -async function isDirectory(path: string) { - const stats = await lstat(path) - return stats.isDirectory() -} - -async function isFile(path: string) { - const stats = await lstat(path) - return stats.isFile() -} -export default class DCStickers extends SplitOut { - async getStickers() { - const stickerFolder = path.join( - this.controller.context.getAccountDir(), - 'stickers' - ) - - let list: string[] = [] - try { - list = await readdir(stickerFolder) - } catch { - log.info(`Sticker folder ${stickerFolder} does not exist`) - return {} - } - - const stickers: { [key: string]: string[] } = {} - for (const stickerPack of list) { - const stickerPackPath: string = path.join(stickerFolder, stickerPack) - if (!(await isDirectory(stickerPackPath))) continue - const stickerImages = [] - for (const sticker of await readdir(stickerPackPath)) { - const stickerPackImagePath = path.join(stickerPackPath, sticker) - if ( - !(sticker.endsWith('.png') || sticker.endsWith('.webp')) || - !(await isFile(stickerPackImagePath)) - ) - continue - stickerImages.push('file://' + stickerPackImagePath) - } - if (stickerImages.length === 0) continue - stickers[stickerPack] = stickerImages - } - - return stickers - } -} diff --git a/src/main/deltachat/webxdc.ts b/src/main/deltachat/webxdc.ts index 52cd6cec4d..46cd0ee4ac 100644 --- a/src/main/deltachat/webxdc.ts +++ b/src/main/deltachat/webxdc.ts @@ -88,7 +88,7 @@ export default class DCWebxdc extends SplitOut { const icon = webxdcInfo.icon const icon_blob = dc_context.getWebxdcBlob(webxdc_message_ref, icon) - const ses = this._currentSession + const ses = sessionFromAccountId(this.selectedAccountId) const appURL = `webxdc://${msg_id}.webxdc` // TODO intercept / deny network access - CSP should probably be disabled for testing @@ -342,12 +342,10 @@ If you think that's a bug and you need that permission, then please open an issu if (instance) { instance.win.close() } - const s = session.fromPartition( - `persist:webxdc_${this.selectedAccountId}`, - { - cache: false, - } - ) + if (!this.selectedAccountId) { + throw new Error('selectedAccountId is empty') + } + const s = sessionFromAccountId(this.selectedAccountId) const appURL = `webxdc://${instanceId}.webxdc` s.clearStorageData({ origin: appURL }) s.clearCodeCaches({ urls: [appURL] }) @@ -363,55 +361,64 @@ If you think that's a bug and you need that permission, then please open an issu } get _currentPartition() { - return `persist:webxdc_${this.selectedAccountId}` + if (!this.selectedAccountId) { + throw new Error('selectedAccountId is empty') + } + return partitionFromAccountId(this.selectedAccountId) } +} - get _currentSession() { - return session.fromPartition(this._currentPartition, { cache: false }) - } +function partitionFromAccountId(accountId: number) { + return `persist:webxdc_${accountId}` +} - clearWebxdcDOMStorage() { - return this._currentSession.clearStorageData() - } +function sessionFromAccountId(accountId: number) { + return session.fromPartition(partitionFromAccountId(accountId), { + cache: false, + }) +} - async getWebxdcDiskUsage() { - const ses = this._currentSession - if (!ses.storagePath) { - throw new Error('session has no storagePath set') - } - const [cache_size, real_total_size] = await Promise.all([ - ses.getCacheSize(), - get_recursive_folder_size(ses.storagePath, [ - 'GPUCache', - 'QuotaManager', - 'Code Cache', - 'LOG', - 'LOG.old', - 'LOCK', - '.DS_Store', - 'Cookies-journal', - 'Databases.db-journal', - 'Preferences', - 'QuotaManager-journal', - '000003.log', - 'MANIFEST-000001', - ]), - ]) - const empty_size = 49 * 1024 // ~ size of an empty session/partition - - let total_size = real_total_size - empty_size - let data_size = total_size - cache_size - if (total_size < 0) { - total_size = 0 - data_size = 0 - } - return { - cache_size, - total_size, - data_size, - } +ipcMain.handle('webxdc.clearWebxdcDOMStorage', (_, accountId: number) => { + sessionFromAccountId(accountId).clearStorageData() +}) + +ipcMain.handle('webxdc.getWebxdcDiskUsage', async (_, accountId: number) => { + const ses = sessionFromAccountId(accountId) + if (!ses.storagePath) { + throw new Error('session has no storagePath set') } -} + const [cache_size, real_total_size] = await Promise.all([ + ses.getCacheSize(), + get_recursive_folder_size(ses.storagePath, [ + 'GPUCache', + 'QuotaManager', + 'Code Cache', + 'LOG', + 'LOG.old', + 'LOCK', + '.DS_Store', + 'Cookies-journal', + 'Databases.db-journal', + 'Preferences', + 'QuotaManager-journal', + '000003.log', + 'MANIFEST-000001', + ]), + ]) + const empty_size = 49 * 1024 // ~ size of an empty session/partition + + let total_size = real_total_size - empty_size + let data_size = total_size - cache_size + if (total_size < 0) { + total_size = 0 + data_size = 0 + } + return { + cache_size, + total_size, + data_size, + } +}) async function get_recursive_folder_size( path: string, diff --git a/src/main/ipc.ts b/src/main/ipc.ts index e4e15ed0c2..8eaaebc422 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -1,19 +1,21 @@ -import { copyFile } from 'fs/promises' -import { app as rawApp, dialog, ipcMain } from 'electron' +import { copyFile, mkdir, mkdtemp, rm } from 'fs/promises' +import { app as rawApp, clipboard, dialog, ipcMain, shell } from 'electron' import { getLogger } from '../shared/logger' import { getLogsPath } from './application-constants' import { LogHandler } from './log-handler' import { ExtendedAppMainProcess } from './types' import * as mainWindow from './windows/main' import { openHelpWindow } from './windows/help' -import path, { basename, join, posix, sep } from 'path' +import path, { basename, extname, join, posix, sep } from 'path' import { DesktopSettings } from './desktop_settings' import { getConfigPath } from './application-constants' import { inspect } from 'util' -import { RuntimeInfo } from '../shared/shared-types' -import { platform } from 'os' +import { DesktopSettingsType, RuntimeInfo } from '../shared/shared-types' +import { platform, tmpdir } from 'os' import { existsSync } from 'fs' -import { set_has_unread } from './tray' +import { set_has_unread, updateTrayIcon } from './tray' +import mimeTypes from 'mime-types' +import { writeFile } from 'fs/promises' const log = getLogger('main/ipc') const DeltaChatController: typeof import('./deltachat/controller').default = (() => { @@ -185,6 +187,21 @@ export async function init(cwd: string, logHandler: LogHandler) { return DesktopSettings.state }) + ipcMain.handle( + 'set-desktop-setting', + ( + _ev, + key: keyof DesktopSettingsType, + value: string | number | boolean | undefined + ) => { + DesktopSettings.update({ [key]: value }) + + if (key === 'minimizeToTray') updateTrayIcon() + + return true + } + ) + ipcMain.handle( 'app.setBadgeCountAndTrayIconIndicator', (_, count: number) => { @@ -193,8 +210,60 @@ export async function init(cwd: string, logHandler: LogHandler) { } ) + ipcMain.handle('app.writeClipboardToTempFile', () => + writeClipboardToTempFile() + ) + + ipcMain.handle( + 'saveBackgroundImage', + async (_ev, file: string, isDefaultPicture: boolean) => { + const originalFilePath = !isDefaultPicture + ? file + : join(__dirname, '../../images/backgrounds/', file) + + const bgDir = join(getConfigPath(), 'background') + await rm(bgDir, { recursive: true, force: true }) + await mkdir(bgDir, { recursive: true }) + const fileName = `background_${Date.now()}` + extname(originalFilePath) + const newPath = join(getConfigPath(), 'background', fileName) + try { + await copyFile(originalFilePath, newPath) + } catch (error) { + log.error('BG-IMG Copy Failed', error) + throw error + } + return `img: ${fileName.replace(/\\/g, '/')}` + } + ) + + ipcMain.handle('openMessageHTML', async (_ev, content: string) => { + const tmpDir = await mkdtemp(join(tmpdir(), 'deltachat-')) + const pathToFile = join(tmpDir, 'message.html') + await writeFile(pathToFile, content, { encoding: 'utf-8' }) + shell.openPath(pathToFile) + }) + return () => { // the shutdown function dcController._inner_account_manager?.stopIO() } } + +async function writeClipboardToTempFile(): Promise { + const formats = clipboard.availableFormats().sort() + log.debug('Clipboard available formats:', formats) + if (formats.length <= 0) { + throw new Error('No files to write') + } + const pathToFile = join( + rawApp.getPath('temp'), + `paste.${mimeTypes.extension(formats[0]) || 'bin'}` + ) + const buf = + mimeTypes.extension(formats[0]) === 'png' + ? clipboard.readImage().toPNG() + : clipboard.readBuffer(formats[0]) + log.debug(`Writing clipboard ${formats[0]} to file ${pathToFile}`) + await writeFile(pathToFile, buf, 'binary') + return pathToFile +} diff --git a/src/main/load-translations.ts b/src/main/load-translations.ts index 74e6f8344e..af9f8d375c 100644 --- a/src/main/load-translations.ts +++ b/src/main/load-translations.ts @@ -10,6 +10,9 @@ import { translate as getTranslateFunction, } from '../shared/localize' +import { refresh as refreshMenu } from './menu' +import { ipcMain } from 'electron' + let currentlocaleData: LocaleData | null = null export function getCurrentLocaleDate(): LocaleData { @@ -85,3 +88,18 @@ function getLocaleMessages(file: string) { throw err } } + +ipcMain.handle( + 'getLocaleData', + (_ev, locale?: string): LocaleData => { + if (locale) { + loadTranslations(locale) + } + return getCurrentLocaleDate() + } +) + +ipcMain.handle('setLocale', (_ev, locale: string) => { + setLanguage(locale) + refreshMenu() +}) diff --git a/src/main/themes.ts b/src/main/themes.ts index 912425a700..1233414b33 100644 --- a/src/main/themes.ts +++ b/src/main/themes.ts @@ -3,7 +3,7 @@ import { readFile, readdir } from 'fs/promises' import { join, basename } from 'path' import { Theme } from '../shared/shared-types' import { getCustomThemesPath } from './application-constants' -import { app as rawApp, nativeTheme } from 'electron' +import { app as rawApp, ipcMain, nativeTheme } from 'electron' import { ExtendedAppMainProcess } from './types' import * as mainWindow from './windows/main' @@ -173,3 +173,19 @@ If you have question or need help, feel free to ask in our forum https://support mainWindow.send('theme-update') }) } + +ipcMain.handle('themes.getActiveTheme', async () => { + try { + log.debug('theme', DesktopSettings.state.activeTheme) + return await loadTheme(DesktopSettings.state.activeTheme) + } catch (error) { + log.error('loading theme failed:', error) + return null + } +}) + +ipcMain.handle('themes.resolveThemeAddress', (_, address: string) => + resolveThemeAddress(address) +) + +ipcMain.handle('themes.getAvailableThemes', getAvailableThemes) diff --git a/src/main/tray.ts b/src/main/tray.ts index 6685833cf4..13615ae080 100644 --- a/src/main/tray.ts +++ b/src/main/tray.ts @@ -22,7 +22,7 @@ export function set_has_unread(new_has_unread: boolean) { } } -export function TrayImage(): string | NativeImage { +function TrayImage(): string | NativeImage { const trayIconFolder = join(__dirname, '..', '..', 'images/tray') if (process.platform === 'darwin') { const image = nativeImage @@ -40,7 +40,7 @@ export function TrayImage(): string | NativeImage { } } -export function mainWindowIsVisible() { +function mainWindowIsVisible() { if (!mainWindow.window) { throw new Error('window does not exist, this should never happen') } @@ -50,13 +50,6 @@ export function mainWindowIsVisible() { return mainWindow.window.isVisible() && mainWindow.window.isFocused() } -export function closeDeltaChat() { - if (!mainWindow.window) { - throw new Error('window does not exist, this should never happen') - } - mainWindow.window.close() -} - export function hideDeltaChat(minimize?: boolean) { if (!mainWindow.window) { throw new Error('window does not exist, this should never happen') @@ -75,11 +68,11 @@ export function showDeltaChat() { mainWindow.window.show() } -export function hideOrShowDeltaChat() { +function hideOrShowDeltaChat() { mainWindowIsVisible() ? hideDeltaChat(true) : showDeltaChat() } -export function quitDeltaChat() { +function quitDeltaChat() { globalShortcut.unregisterAll() app.quit() } @@ -94,13 +87,13 @@ export function updateTrayIcon() { renderTrayIcon() } -export function destroyTrayIcon() { +function destroyTrayIcon() { log.info('destroy icon tray') tray?.destroy() tray = null } -export function getTrayMenu() { +function getTrayMenu() { if (tray === null) return if (process.platform === 'darwin') { contextMenu = Menu.buildFromTemplate([ @@ -165,11 +158,11 @@ export function getTrayMenu() { return contextMenu } -export function TrayIcon() { +function TrayIcon() { return new Tray(TrayImage()) } -export function renderTrayIcon() { +function renderTrayIcon() { if (tray != null) { log.warn('Tray icon not destroyed before render?') destroyTrayIcon() diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 04ab9aeff2..e064fb6c7f 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,16 +1,19 @@ import React, { useState, useEffect, useLayoutEffect } from 'react' import { i18nContext } from './contexts' import ScreenController from './ScreenController' -import { sendToBackend, ipcBackend, startBackendLogging } from './ipc' +import { sendToBackend, ipcBackend } from './ipc' import attachKeybindingsListener from './keybindings' import { translate, LocaleData } from '../shared/localize' -import { DeltaBackend } from './delta-remote' import { ThemeManager, ThemeContext } from './ThemeManager' import moment from 'moment' import { CrashScreen } from './components/screens/CrashScreen' import { runtime } from './runtime' +import { updateCoreStrings } from './stockStrings' +import { getLogger } from '../shared/logger' +import { BackendRemote } from './backend-com' +import { runPostponedFunctions } from './onready' attachKeybindingsListener() @@ -49,6 +52,7 @@ export default function App(_props: any) { useLayoutEffect(() => { startBackendLogging() + runPostponedFunctions() ;(async () => { const desktop_settings = await runtime.getDesktopSettings() await reloadLocaleData(desktop_settings.locale || 'en') @@ -56,19 +60,17 @@ export default function App(_props: any) { }, []) async function reloadLocaleData(locale: string) { - const localeData: LocaleData = await DeltaBackend.call( - 'extras.getLocaleData', - locale - ) + const localeData = await runtime.getLocaleData(locale) window.localeData = localeData window.static_translate = translate(localeData.messages) setLocaleData(localeData) moment.locale(localeData.locale) + updateCoreStrings() } useEffect(() => { const onChooseLanguage = async (_e: any, locale: string) => { - await DeltaBackend.call('extras.setLocale', locale) + await runtime.setLocale(locale) await reloadLocaleData(locale) } ipcBackend.on('chooseLanguage', onChooseLanguage) @@ -93,9 +95,63 @@ function ThemeContextWrapper({ children }: { children: React.ReactChild }) { const [theme_rand, setThemeRand] = useState(0) useEffect(() => { ThemeManager.setUpdateHook(() => setThemeRand(Math.random())) + ThemeManager.refresh() }, []) return ( {children} ) } + +const log = getLogger('renderer/App') +let backendLoggingStarted = false +export function startBackendLogging() { + if (backendLoggingStarted === true) + return log.error('Backend logging is already started!') + backendLoggingStarted = true + + const log2 = getLogger('renderer') + window.addEventListener('error', event => { + log2.error('Unhandled Error:', event.error) + }) + window.addEventListener('unhandledrejection', event => { + log2.error('Unhandled Rejection:', event, event.reason) + }) + + const rc = runtime.getRC_Config() + if (rc['log-debug']) { + BackendRemote.on('ALL', (accountId, event) => { + const isActiveAccount = window.__selectedAccountId == accountId + let data: any, + accountColor = 'rgba(125,125,125,0.25)' + const eventColor = 'rgba(125,125,125,0.15)' + + if (isActiveAccount) { + accountColor = 'rgba(0,125,0,0.25)' + } + + if ( + event.type == 'Info' || + event.type == 'Warning' || + event.type == 'Error' + ) { + data = event.msg + } else if (event.type == 'ConnectivityChanged') { + // has no arguments + data = '' + } else { + const event_clone = Object.assign({}, event) as Partial + delete event_clone.type + data = event_clone + } + + /* ignore-console-log */ + console.debug( + `%c${isActiveAccount ? '👤' : '👻'}${accountId}%c📡 ${event.type}`, + `background:${accountColor};border-radius:8px 0 0 8px;padding:2px 4px;`, + `background:${eventColor};border-radius:0 2px 2px 0;padding:2px 4px;`, + data + ) + }) + } +} diff --git a/src/renderer/ScreenController.tsx b/src/renderer/ScreenController.tsx index 57ea77dacb..518221a62c 100644 --- a/src/renderer/ScreenController.tsx +++ b/src/renderer/ScreenController.tsx @@ -19,6 +19,9 @@ import AccountListScreen from './components/screens/AccountListScreen' import WelcomeScreen from './components/screens/WelcomeScreen' import { BackendRemote } from './backend-com' import { debouncedUpdateBadgeCounter } from './system-integration/badge-counter' +import { hintUpdateIfNessesary, updateDeviceChats } from './deviceMessages' +import { runtime } from './runtime' +import { DcEventType } from '@deltachat/jsonrpc-client' const log = getLogger('renderer/ScreenController') @@ -74,10 +77,8 @@ export default class ScreenController extends Component { } private async startup() { - const lastLoggedInAccountId = await DeltaBackend.call( - 'login.getLastLoggedInAccount' - ) - if (lastLoggedInAccountId && !(lastLoggedInAccountId < 0)) { + const lastLoggedInAccountId = await this._getLastUsedAccount() + if (lastLoggedInAccountId) { await this.selectAccount(lastLoggedInAccountId) } else { const allAccountIds = await BackendRemote.rpc.getAllAccountIds() @@ -90,6 +91,24 @@ export default class ScreenController extends Component { } } + private async _getLastUsedAccount(): Promise { + const lastLoggedInAccountId = (await runtime.getDesktopSettings()) + .lastAccount + try { + if (lastLoggedInAccountId) { + await BackendRemote.rpc.getAccountInfo(lastLoggedInAccountId) + return lastLoggedInAccountId + } else { + return undefined + } + } catch (error) { + log.warn( + `getLastUsedAccount: account with id ${lastLoggedInAccountId} does not exist` + ) + return undefined + } + } + async selectAccount(accountId: number) { // for now we still need to call the backend function, // because backend still has sleected account @@ -102,6 +121,8 @@ export default class ScreenController extends Component { ) if (account.type === 'Configured') { this.changeScreen(Screens.Main) + hintUpdateIfNessesary(this.selectedAccountId) + updateDeviceChats(this.selectedAccountId) } else { this.changeScreen(Screens.Welcome) } @@ -128,9 +149,7 @@ export default class ScreenController extends Component { } componentDidMount() { - ipcRenderer.on('error', this.onError) - ipcRenderer.on('DC_EVENT_ERROR', this.onError) - ipcRenderer.on('success', this.onSuccess) + BackendRemote.on('Error', this.onError) ipcRenderer.on('showAboutDialog', this.onShowAbout) ipcRenderer.on('showKeybindingsDialog', this.onShowKeybindings) ipcRenderer.on('showSettingsDialog', this.onShowSettings) @@ -142,19 +161,20 @@ export default class ScreenController extends Component { } componentWillUnmount() { + BackendRemote.off('Error', this.onError) ipcRenderer.removeListener('showAboutDialog', this.onShowAbout) ipcRenderer.removeListener('showSettingsDialog', this.onShowSettings) - ipcRenderer.removeListener('error', this.onError) - ipcRenderer.removeListener('DC_EVENT_ERROR', this.onError) - ipcRenderer.removeListener('success', this.onSuccess) ipcRenderer.removeListener('open-url', this.onOpenUrl) } - onError(_event: any, [data1, data2]: [string | number, string]) { - if (this.state.screen === Screens.Welcome) return - if (data1 === 0) data1 = '' - const text = data1 + data2 - this.userFeedback({ type: 'error', text }) + onError(accountId: number, { msg }: DcEventType<'Error'>) { + if ( + this.selectedAccountId !== accountId || + this.state.screen === Screens.Welcome + ) { + return + } + this.userFeedback({ type: 'error', text: msg }) } onSuccess(_event: any, text: string) { diff --git a/src/renderer/ThemeManager.tsx b/src/renderer/ThemeManager.tsx index 2d0d240ccd..4a4a099528 100644 --- a/src/renderer/ThemeManager.tsx +++ b/src/renderer/ThemeManager.tsx @@ -1,7 +1,7 @@ -import { DeltaBackend } from './delta-remote' import { Theme } from '../shared/shared-types' import { ipcBackend } from './ipc' import React, { useContext, useMemo } from 'react' +import { runtime } from './runtime' export namespace ThemeManager { let currentThemeMetaData: Theme @@ -11,7 +11,7 @@ export namespace ThemeManager { const theme: { theme: Theme data: string - } | null = await DeltaBackend.call('extras.getActiveTheme') + } | null = await runtime.getActiveTheme() if (theme) { currentThemeMetaData = theme.theme const themeVars = window.document.getElementById('theme-vars') @@ -24,7 +24,6 @@ export namespace ThemeManager { } ipcBackend.on('theme-update', _e => refresh()) - refresh() export function getCurrentThemeMetaData() { return currentThemeMetaData diff --git a/src/renderer/backend-com.ts b/src/renderer/backend-com.ts index 536daf25cd..64e64a9f85 100644 --- a/src/renderer/backend-com.ts +++ b/src/renderer/backend-com.ts @@ -1,4 +1,4 @@ -import { BaseDeltaChat, yerpc, RPC } from '@deltachat/jsonrpc-client' +import { BaseDeltaChat, yerpc, RPC, DcEvent } from '@deltachat/jsonrpc-client' import { DeltaBackend } from './delta-remote' import { runtime } from './runtime' import { getLogger } from '../shared/logger' @@ -17,7 +17,7 @@ class ElectronTransport extends BaseTransport { 'json-rpc-message', (_ev, response) => { const message: RPC.Message = JSON.parse(response) - // log.debug("received: ", message) + log.debug('received: ', message) this._onmessage(message) } ) @@ -65,8 +65,14 @@ export namespace EffectfulBackendActions { runtime.closeAllWebxdcInstances() debouncedUpdateBadgeCounter() + if (!(await runtime.getDesktopSettings()).syncAllAccounts) { + await BackendRemote.rpc.stopIo(window.__selectedAccountId) + } + + runtime.setDesktopSetting('lastAccount', undefined) + // for now we still need to call the backend function, - // because backend still has sleected account + // because backend still has selected account await DeltaBackend.call('login.logout') ;(window.__selectedAccountId as any) = undefined } @@ -88,3 +94,21 @@ export namespace EffectfulBackendActions { window.__refetchChatlist && window.__refetchChatlist() } } + +type ContextEvents = { ALL: (event: DcEvent) => void } & { + [Property in DcEvent['type']]: ( + event: Extract + ) => void +} + +export function onDCEvent( + accountId: number, + eventType: variant, + callback: ContextEvents[variant] +) { + const emitter = BackendRemote.getContextEvents(accountId) + emitter.on(eventType, callback) + return () => { + emitter.off(eventType, callback) + } +} diff --git a/src/renderer/components/ConnectivityToast.tsx b/src/renderer/components/ConnectivityToast.tsx index 8f19ed566d..544692bcf0 100644 --- a/src/renderer/components/ConnectivityToast.tsx +++ b/src/renderer/components/ConnectivityToast.tsx @@ -5,7 +5,6 @@ import React, { useMemo, useContext, } from 'react' -import { onDCEvent } from '../ipc' import { getLogger } from '../../shared/logger' import { ScreenContext, useTranslationFunction } from '../contexts' @@ -15,7 +14,7 @@ import { C } from 'deltachat-node/node/dist/constants' import { debounce } from 'debounce' import { debounceWithInit } from './chat/ChatListHelpers' import SettingsConnectivityDialog from './dialogs/Settings-Connectivity' -import { BackendRemote } from '../backend-com' +import { BackendRemote, onDCEvent } from '../backend-com' import { selectedAccountId } from '../ScreenController' const log = getLogger('renderer/components/ConnectivityToast') @@ -89,7 +88,7 @@ export default function ConnectivityToast() { const onConnectivityChanged = useMemo( () => - debounceWithInit(async (_data1: any, _data2: any) => { + debounceWithInit(async () => { const connectivity = await BackendRemote.rpc.getConnectivity(accountId) if (connectivity >= C.DC_CONNECTIVITY_CONNECTED) { @@ -131,7 +130,8 @@ export default function ConnectivityToast() { window.addEventListener('focus', maybeNetwork) const removeOnConnectivityChanged = onDCEvent( - 'DC_EVENT_CONNECTIVITY_CHANGED', + accountId, + 'ConnectivityChanged', onConnectivityChanged ) @@ -142,7 +142,7 @@ export default function ConnectivityToast() { removeOnConnectivityChanged() } - }, [onBrowserOnline, maybeNetwork, onConnectivityChanged]) + }, [onBrowserOnline, maybeNetwork, onConnectivityChanged, accountId]) const onTryReconnectClick = (ev: React.MouseEvent) => { ev.preventDefault() diff --git a/src/renderer/components/LoginForm.tsx b/src/renderer/components/LoginForm.tsx index ccb07f651b..ea9179c5e2 100644 --- a/src/renderer/components/LoginForm.tsx +++ b/src/renderer/components/LoginForm.tsx @@ -12,14 +12,14 @@ import { import { Collapse, Dialog } from '@blueprintjs/core' import ClickableLink from './helpers/ClickableLink' import { DialogProps } from './dialogs/DialogController' -import { ipcBackend } from '../ipc' import { DeltaDialogContent, DeltaDialogFooter } from './dialogs/DeltaDialog' import { Credentials } from '../../shared/shared-types' import { useTranslationFunction, i18nContext } from '../contexts' import { useDebouncedCallback } from 'use-debounce/lib' import { getLogger } from '../../shared/logger' -import { IpcRendererEvent } from 'electron/renderer' import { BackendRemote, Type } from '../backend-com' +import { selectedAccountId } from '../ScreenController' +import { DcEventType } from '@deltachat/jsonrpc-client' const log = getLogger('renderer/loginForm') @@ -414,13 +414,14 @@ export function ConfigureProgressDialog({ const [progressComment, setProgressComment] = useState('') const [error, setError] = useState('') const [configureFailed, setConfigureFailed] = useState(false) + const accountId = selectedAccountId() - const onConfigureProgress = ( - _: IpcRendererEvent | null, - [progress, comment]: [number, string] - ) => { + const onConfigureProgress = ({ + progress, + comment, + }: DcEventType<'ConfigureProgress'>) => { progress !== 0 && setProgress(progress) - setProgressComment(comment) + setProgressComment(comment || '') } const onCancel = async (_event: any) => { @@ -431,25 +432,14 @@ export function ConfigureProgressDialog({ await BackendRemote.rpc.stopOngoingProcess(window.__selectedAccountId) } catch (error: any) { log.error('failed to stopOngoingProcess', error) - onConfigureError(null, [ - null, - 'failed to stopOngoingProcess' + error.message || error.toString(), - ]) - onConfigureFailed(null, [null, '']) + setError( + 'failed to stopOngoingProcess' + error.message || error.toString() + ) + setConfigureFailed(true) } onClose() } - const onConfigureError = ( - _: IpcRendererEvent | null, - [_data1, data2]: [null, string] - ) => setError(data2) - - const onConfigureFailed = ( - _: IpcRendererEvent | null, - [_data1, _data2]: [null, string] - ) => setConfigureFailed(true) - useEffect( () => { ;(async () => { @@ -469,8 +459,8 @@ export function ConfigureProgressDialog({ onSuccess && onSuccess() } catch (err: any) { log.error('configure error', err) - onConfigureError(null, [null, err.message || err.toString()]) - onConfigureFailed(null, [null, '']) + setError(err.message || err.toString()) + setConfigureFailed(true) } })() }, @@ -478,16 +468,12 @@ export function ConfigureProgressDialog({ ) useEffect(() => { - ipcBackend.on('DC_EVENT_CONFIGURE_PROGRESS', onConfigureProgress) - ipcBackend.on('DCN_EVENT_CONFIGURE_FAILED', onConfigureFailed) + const emitter = BackendRemote.getContextEvents(accountId) + emitter.on('ConfigureProgress', onConfigureProgress) return () => { - ipcBackend.removeListener( - 'DC_EVENT_CONFIGURE_PROGRESS', - onConfigureProgress - ) - ipcBackend.removeListener('DCN_EVENT_CONFIGURE_FAILED', onConfigureFailed) + emitter.off('ConfigureProgress', onConfigureProgress) } - }, []) + }, [accountId]) const tx = useTranslationFunction() diff --git a/src/renderer/components/Sidebar.tsx b/src/renderer/components/Sidebar.tsx index 2b010c865c..8a65a84ca2 100644 --- a/src/renderer/components/Sidebar.tsx +++ b/src/renderer/components/Sidebar.tsx @@ -13,8 +13,11 @@ import { VERSION } from '../../shared/build-info' import { ActionEmitter, KeybindAction } from '../keybindings' import SettingsConnectivityDialog from './dialogs/Settings-Connectivity' import { debounceWithInit } from './chat/ChatListHelpers' -import { onDCEvent } from '../ipc' -import { BackendRemote, EffectfulBackendActions } from '../backend-com' +import { + BackendRemote, + EffectfulBackendActions, + onDCEvent, +} from '../backend-com' export type SidebarState = 'init' | 'visible' | 'invisible' @@ -217,32 +220,32 @@ export function Link({ export default Sidebar const SidebarConnectivity = () => { - const [state, setState] = useState('') - const tx = window.static_translate + const [state, setState] = useState('') const accountId = selectedAccountId() const onConnectivityChanged = useMemo( () => - debounceWithInit(async (_data1: any, _data2: any) => { + debounceWithInit(async () => { + const tx = window.static_translate const connectivity = await BackendRemote.rpc.getConnectivity(accountId) if (connectivity >= C.DC_CONNECTIVITY_CONNECTED) { - setState('connectivity_connected') + setState(tx('connectivity_connected')) } else if (connectivity >= C.DC_CONNECTIVITY_WORKING) { - setState('connectivity_updating') + setState(tx('connectivity_updating')) } else if (connectivity >= C.DC_CONNECTIVITY_CONNECTING) { - setState('connectivity_connecting') + setState(tx('connectivity_connecting')) } else if (connectivity >= C.DC_CONNECTIVITY_NOT_CONNECTED) { - setState('connectivity_not_connected') + setState(tx('connectivity_not_connected')) } }, 300), [accountId] ) useEffect( - () => onDCEvent('DC_EVENT_CONNECTIVITY_CHANGED', onConnectivityChanged), - [onConnectivityChanged] + () => onDCEvent(accountId, 'ConnectivityChanged', onConnectivityChanged), + [onConnectivityChanged, accountId] ) - return <>{tx(state)} + return <>{state} } diff --git a/src/renderer/components/ThreeDotMenu.tsx b/src/renderer/components/ThreeDotMenu.tsx index 1e47e667e3..53207fbd10 100644 --- a/src/renderer/components/ThreeDotMenu.tsx +++ b/src/renderer/components/ThreeDotMenu.tsx @@ -13,6 +13,7 @@ import { import { ContextMenuItem } from './ContextMenu' import { useSettingsStore } from '../stores/settings' import { Type } from '../backend-com' +import { ActionEmitter, KeybindAction } from '../keybindings' export function DeltaMenuItem({ text, @@ -62,6 +63,17 @@ export function useThreeDotMenu(selectedChat: Type.FullChat | null) { }) menu = [ + { + label: tx('search_in_chat'), + action: () => { + window.__chatlistSetSearch?.('', selectedChat.id) + setTimeout( + () => + ActionEmitter.emitAction(KeybindAction.ChatList_FocusSearchInput), + 0 + ) + }, + }, canSend && selectedChat.chatType !== C.DC_CHAT_TYPE_MAILINGLIST && { label: tx('ephemeral_messages'), @@ -85,13 +97,11 @@ export function useThreeDotMenu(selectedChat: Type.FullChat | null) { selectedChat.archived ? { label: tx('menu_unarchive_chat'), - action: () => - setChatVisibility(chatId, C.DC_CHAT_VISIBILITY_NORMAL, true), + action: () => setChatVisibility(chatId, 'Normal', true), } : { label: tx('menu_archive_chat'), - action: () => - setChatVisibility(chatId, C.DC_CHAT_VISIBILITY_ARCHIVED, true), + action: () => setChatVisibility(chatId, 'Archived', true), }, !isGroup && !(isSelfTalk || isDeviceChat) && { diff --git a/src/renderer/components/chat/ChatList.tsx b/src/renderer/components/chat/ChatList.tsx index b2bc1af43b..cd45b79bad 100644 --- a/src/renderer/components/chat/ChatList.tsx +++ b/src/renderer/components/chat/ChatList.tsx @@ -16,9 +16,7 @@ import { } from './ChatListItemRow' import { PseudoListItemAddContact } from '../helpers/PseudoListItem' import { C } from 'deltachat-node/node/dist/constants' -import { DeltaBackend } from '../../delta-remote' import { useContactIds } from '../contact/ContactList' -import { MessageSearchResult } from '../../../shared/shared-types' import AutoSizer from 'react-virtualized-auto-sizer' import { FixedSizeList as List, @@ -27,8 +25,7 @@ import { } from 'react-window' import InfiniteLoader from 'react-window-infinite-loader' -import { onDCEvent } from '../../ipc' -import { ScreenContext } from '../../contexts' +import { ScreenContext, useTranslationFunction } from '../../contexts' import { KeybindAction, useKeyBindingAction } from '../../keybindings' import { @@ -36,8 +33,10 @@ import { selectChat, } from '../helpers/ChatMethods' import { useThemeCssVar } from '../../ThemeManager' -import { BackendRemote, Type } from '../../backend-com' +import { BackendRemote, onDCEvent, Type } from '../../backend-com' import { selectedAccountId } from '../../ScreenController' +import { DcEvent, DcEventType, T } from '@deltachat/jsonrpc-client' +import { Avatar } from '../Avatar' const enum LoadStatus { FETCHING = 1, @@ -99,10 +98,19 @@ export default function ChatList(props: { selectedChatId: number | null showArchivedChats: boolean queryStr?: string + queryChatId: number | null onExitSearch?: () => void onChatClick: (chatId: number) => void }) { - const { selectedChatId, showArchivedChats, onChatClick, queryStr } = props + const accountId = selectedAccountId() + + const { + selectedChatId, + showArchivedChats, + onChatClick, + queryStr, + queryChatId, + } = props const isSearchActive = queryStr !== '' const { @@ -115,7 +123,7 @@ export default function ChatList(props: { loadContact, contactCache, queryStrIsValidEmail, - } = useContactAndMessageLogic(queryStr) + } = useContactAndMessageLogic(queryStr, queryChatId) const { chatListIds, isChatLoaded, loadChats, chatCache } = useLogicChatPart( queryStr, @@ -269,7 +277,76 @@ export default function ChatList(props: { return { messageResultIds, messageCache, openDialog, queryStr } }, [messageResultIds, messageCache, openDialog, queryStr]) + const [searchChatInfo, setSearchChatInfo] = useState(null) + useEffect(() => { + if (queryChatId) { + BackendRemote.rpc + .chatlistGetFullChatById(accountId, queryChatId) + .then(setSearchChatInfo) + .catch(console.error) + } else { + setSearchChatInfo(null) + } + }, [accountId, queryChatId, isSearchActive]) + // Render -------------------- + const tx = useTranslationFunction() + + if (queryChatId && searchChatInfo) { + return ( + <> +
+ + {({ width, height }) => ( +
+
+ {tx('search_in_chat')} +
+
+ +
{searchChatInfo.name}
+ +
+
+ {translate_n('n_messages', messageResultIds.length)} +
+ 'key' + messageResultIds[index]} + itemData={messagelistData} + itemHeight={CHATLISTITEM_MESSAGE_HEIGHT} + > + {ChatListItemRowMessage} + +
+ )} +
+
+ + ) + } + return ( <>
@@ -331,7 +408,7 @@ export default function ChatList(props: { className='search-result-divider' style={{ width: width }} > - {translate_n('n_messages', messageResultIds.length)} + {translated_messages_label(messageResultIds.length)}
{ + return ({ + chatId, + }: Extract< + DcEvent, + { + type: + | 'MsgRead' + | 'MsgDelivered' + | 'MsgFailed' + | 'IncomingMsg' + | 'ChatModified' + | 'MsgsChanged' + | 'MsgsNoticed' + } + >) => { if (chatId === C.DC_CHAT_ID_TRASH) { return } @@ -492,7 +583,7 @@ export function useLogicVirtualChatList(chatListIds: [number, number][]) { * Currently used for updating nickname changes in the summary of chatlistitems. */ const onContactChanged = useCallback( - async (contactId: number) => { + async ({ contactId }: DcEventType<'ContactsChanged'>) => { if (contactId !== 0) { const chatListItems = await BackendRemote.rpc.getChatlistEntries( accountId, @@ -516,30 +607,32 @@ export function useLogicVirtualChatList(chatListIds: [number, number][]) { [accountId] ) + useEffect(() => onDCEvent(accountId, 'ContactsChanged', onContactChanged), [ + accountId, + onContactChanged, + ]) + useEffect(() => { - const removeOnChatListItemChangedListener = onDCEvent( - [ - 'DC_EVENT_MSG_READ', - 'DC_EVENT_MSG_DELIVERED', - 'DC_EVENT_MSG_FAILED', - 'DC_EVENT_CHAT_MODIFIED', - 'DC_EVENT_INCOMING_MSG', - 'DC_EVENT_MSGS_CHANGED', - 'DC_EVENT_MSGS_NOTICED', - ], - onChatListItemChanged - ) + const emitter = BackendRemote.getContextEvents(accountId) - const removeOnContactChangedListener = onDCEvent( - 'DC_EVENT_CONTACTS_CHANGED', - onContactChanged - ) + emitter.on('MsgRead', onChatListItemChanged) + emitter.on('MsgDelivered', onChatListItemChanged) + emitter.on('MsgFailed', onChatListItemChanged) + emitter.on('IncomingMsg', onChatListItemChanged) + emitter.on('ChatModified', onChatListItemChanged) + emitter.on('MsgsChanged', onChatListItemChanged) + emitter.on('MsgsNoticed', onChatListItemChanged) return () => { - removeOnChatListItemChangedListener() - removeOnContactChangedListener() + emitter.off('MsgRead', onChatListItemChanged) + emitter.off('MsgDelivered', onChatListItemChanged) + emitter.off('MsgFailed', onChatListItemChanged) + emitter.off('IncomingMsg', onChatListItemChanged) + emitter.off('ChatModified', onChatListItemChanged) + emitter.off('MsgsChanged', onChatListItemChanged) + emitter.off('MsgsNoticed', onChatListItemChanged) } - }, [onChatListItemChanged, onContactChanged]) + }, [onChatListItemChanged, accountId]) // effects @@ -576,10 +669,13 @@ function useLogicChatPart( return { chatListIds, isChatLoaded, loadChats, chatCache } } -function useContactAndMessageLogic(queryStr: string | undefined) { +function useContactAndMessageLogic( + queryStr: string | undefined, + searchChatId: number | null = null +) { const accountId = selectedAccountId() const { contactIds, queryStrIsValidEmail } = useContactIds(0, queryStr) - const messageResultIds = useMessageResults(queryStr) + const messageResultIds = useMessageResults(queryStr, searchChatId) // Contacts ---------------- const [contactCache, setContactCache] = useState<{ @@ -615,7 +711,7 @@ function useContactAndMessageLogic(queryStr: string | undefined) { // Message ---------------- const [messageCache, setMessageCache] = useState<{ - [id: number]: MessageSearchResult + [id: number]: T.MessageSearchResult }>({}) const [messageLoadState, setMessageLoading] = useState<{ [id: number]: undefined | LoadStatus.FETCHING | LoadStatus.LOADED @@ -633,8 +729,8 @@ function useContactAndMessageLogic(queryStr: string | undefined) { ids.forEach(id => (state[id] = LoadStatus.FETCHING)) return state }) - const messages = await DeltaBackend.call( - 'messageList.msgIds2SearchResultItems', + const messages = await BackendRemote.rpc.messageIdsToSearchResults( + accountId, ids ) setMessageCache(cache => ({ ...cache, ...messages })) @@ -664,3 +760,12 @@ function useContactAndMessageLogic(queryStr: string | undefined) { queryStrIsValidEmail, } } + +function translated_messages_label(count: number) { + // the search function truncates search to 1000 items for global search + if (count === 1000) { + return window.static_translate('n_messages', '1000+', { quantity: 'other' }) + } else { + return translate_n('n_messages', count) + } +} diff --git a/src/renderer/components/chat/ChatListContextMenu.tsx b/src/renderer/components/chat/ChatListContextMenu.tsx index 5c9e9615dc..00b9e1b342 100644 --- a/src/renderer/components/chat/ChatListContextMenu.tsx +++ b/src/renderer/components/chat/ChatListContextMenu.tsx @@ -27,30 +27,19 @@ function archiveStateMenu( ): ContextMenuItem[] { const archive: ContextMenuItem = { label: tx('menu_archive_chat'), - action: () => - setChatVisibility( - chat.id, - C.DC_CHAT_VISIBILITY_ARCHIVED, - isTheSelectedChat - ), + action: () => setChatVisibility(chat.id, 'Archived', isTheSelectedChat), } const unArchive: ContextMenuItem = { label: tx('menu_unarchive_chat'), - action: () => - setChatVisibility( - chat.id, - C.DC_CHAT_VISIBILITY_NORMAL, - isTheSelectedChat - ), + action: () => setChatVisibility(chat.id, 'Normal', isTheSelectedChat), } const pin: ContextMenuItem = { label: tx('pin_chat'), - action: () => - setChatVisibility(chat.id, C.DC_CHAT_VISIBILITY_PINNED, chat.isArchived), + action: () => setChatVisibility(chat.id, 'Pinned', chat.isArchived), } const unPin: ContextMenuItem = { label: tx('unpin_chat'), - action: () => setChatVisibility(chat.id, C.DC_CHAT_VISIBILITY_NORMAL), + action: () => setChatVisibility(chat.id, 'Normal'), } /* diff --git a/src/renderer/components/chat/ChatListHelpers.tsx b/src/renderer/components/chat/ChatListHelpers.tsx index 3d905e1ef7..eeaf2d963c 100644 --- a/src/renderer/components/chat/ChatListHelpers.tsx +++ b/src/renderer/components/chat/ChatListHelpers.tsx @@ -1,9 +1,8 @@ import { useState, useEffect, useMemo } from 'react' -import { ipcBackend } from '../../ipc' import { getLogger } from '../../../shared/logger' -import { DeltaBackend } from '../../delta-remote' import { debounce } from 'debounce' import { BackendRemote } from '../../backend-com' +import { selectedAccountId } from '../../ScreenController' const log = getLogger('renderer/helpers/ChatList') @@ -24,17 +23,20 @@ export function debounceWithInit>( } } -export function useMessageResults(queryStr: string | undefined) { +export function useMessageResults( + queryStr: string | undefined, + chatId: number | null = null +) { const [ids, setIds] = useState([]) const debouncedSearchMessages = useMemo( () => debounceWithInit((queryStr: string | undefined) => { - DeltaBackend.call('messageList.searchMessages', queryStr || '', 0).then( - setIds - ) + BackendRemote.rpc + .searchMessages(selectedAccountId(), queryStr || '', chatId) + .then(setIds) }, 200), - [] + [chatId] ) useEffect(() => debouncedSearchMessages(queryStr), [ @@ -94,13 +96,24 @@ export function useChatList( } window.__refetchChatlist = refetchChatlist - ipcBackend.on('DD_EVENT_CHATLIST_CHANGED', refetchChatlist) + const emitter = BackendRemote.getContextEvents(accountId) + emitter.on('MsgsChanged', refetchChatlist) + emitter.on('IncomingMsg', refetchChatlist) + emitter.on('ChatModified', refetchChatlist) debouncedGetChatListEntries(listFlags, queryStr, queryContactId) return () => { - ipcBackend.removeListener('DD_EVENT_CHATLIST_CHANGED', refetchChatlist) + emitter.off('MsgsChanged', refetchChatlist) + emitter.off('IncomingMsg', refetchChatlist) + emitter.off('ChatModified', refetchChatlist) window.__refetchChatlist = undefined } - }, [listFlags, queryStr, queryContactId, debouncedGetChatListEntries]) + }, [ + listFlags, + queryStr, + queryContactId, + debouncedGetChatListEntries, + accountId, + ]) return { chatListIds: chatListEntries, diff --git a/src/renderer/components/chat/ChatListItem.tsx b/src/renderer/components/chat/ChatListItem.tsx index 15306da0fe..a817a2ad07 100644 --- a/src/renderer/components/chat/ChatListItem.tsx +++ b/src/renderer/components/chat/ChatListItem.tsx @@ -3,10 +3,10 @@ import classNames from 'classnames' import Timestamp from '../conversations/Timestamp' import MessageBody from '../message/MessageBody' import { C } from 'deltachat-node/node/dist/constants' -import { MessageSearchResult } from '../../../shared/shared-types' import { Avatar } from '../Avatar' import { Type } from '../../backend-com' import { mapCoreMsgStatus2String } from '../helpers/MapMsgStatus' +import { T } from '@deltachat/jsonrpc-client' function FreshMessageCounter({ counter }: { counter: number }) { if (counter === 0) return null @@ -263,7 +263,7 @@ const ChatListItem = React.memo( export default ChatListItem export const ChatListItemMessageResult = React.memo<{ - msr: MessageSearchResult + msr: T.MessageSearchResult onClick: () => void queryStr: string }>(props => { @@ -273,14 +273,14 @@ export const ChatListItemMessageResult = React.memo<{
- {msr.author_name + (msr.chat_name ? ' in ' + msr.chat_name : '')} + {msr.authorName + (msr.chatName ? ' in ' + msr.chatName : '')}
diff --git a/src/renderer/components/composer/Composer.tsx b/src/renderer/components/composer/Composer.tsx index ce383deb1c..c0c2ffd9bb 100644 --- a/src/renderer/components/composer/Composer.tsx +++ b/src/renderer/components/composer/Composer.tsx @@ -14,13 +14,13 @@ import { EmojiAndStickerPicker } from './EmojiAndStickerPicker' import { EmojiData, BaseEmoji } from 'emoji-mart' import { replaceColonsSafe } from '../conversations/emoji' import { Quote } from '../message/Message' -import { DeltaBackend } from '../../delta-remote' import { DraftAttachment } from '../attachment/messageAttachment' import { sendMessage, unselectChat } from '../helpers/ChatMethods' import { useSettingsStore } from '../../stores/settings' import { BackendRemote, EffectfulBackendActions, Type } from '../../backend-com' import { selectedAccountId } from '../../ScreenController' import { MessageTypeAttachmentSubset } from '../attachment/Attachment' +import { runtime } from '../../runtime' const log = getLogger('renderer/composer') @@ -181,7 +181,8 @@ const Composer = forwardRef< try { // Write clipboard to file then attach it to the draft - addFileToDraft(await DeltaBackend.call('extras.writeClipboardToTempFile')) + const path = await runtime.writeClipboardToTempFile() + addFileToDraft(path) } catch (err) { log.error('Failed to paste file.', err) } diff --git a/src/renderer/components/composer/EmojiAndStickerPicker.tsx b/src/renderer/components/composer/EmojiAndStickerPicker.tsx index 9f472a985a..30c63052b4 100644 --- a/src/renderer/components/composer/EmojiAndStickerPicker.tsx +++ b/src/renderer/components/composer/EmojiAndStickerPicker.tsx @@ -7,9 +7,11 @@ import React, { } from 'react' import { Picker, EmojiData } from 'emoji-mart' import classNames from 'classnames' -import { DeltaBackend } from '../../delta-remote' import { ActionEmitter, KeybindAction } from '../../keybindings' import { useTranslationFunction } from '../../contexts' +import { BackendRemote } from '../../backend-com' +import { selectedAccountId } from '../../ScreenController' +import { runtime } from '../../runtime' export const StickerDiv = (props: { stickerPackName: string @@ -24,7 +26,8 @@ export const StickerDiv = (props: { setShowEmojiPicker, } = props const onClickSticker = (fileName: string) => { - DeltaBackend.call('messageList.sendSticker', chatId, fileName) + const stickerPath = fileName.replace('file://', '') + BackendRemote.rpc.sendSticker(selectedAccountId(), chatId, stickerPath) setShowEmojiPicker(false) } @@ -58,8 +61,17 @@ export const StickerPicker = (props: { setShowEmojiPicker: (enabled: boolean) => void }) => { const { stickers, chatId, setShowEmojiPicker } = props + + const onOpenStickerFolder = async () => { + const folder = await BackendRemote.rpc.miscGetStickerFolder( + selectedAccountId() + ) + runtime.openPath(folder) + } + return (
+
{Object.keys(stickers).map(stickerPackName => { return ( @@ -104,6 +116,7 @@ export const EmojiAndStickerPicker = forwardRef< setShowEmojiPicker: React.Dispatch> } >((props, ref) => { + const accountId = selectedAccountId() const { onEmojiSelect, chatId, setShowEmojiPicker } = props const tx = useTranslationFunction() @@ -111,13 +124,12 @@ export const EmojiAndStickerPicker = forwardRef< const [stickers, setStickers] = useState<{ [key: string]: string[] }>({}) - const disableStickers = Object.keys(stickers).length === 0 useEffect(() => { - DeltaBackend.call('stickers.getStickers').then(stickers => - setStickers(stickers) - ) - }, []) + BackendRemote.rpc + .miscGetStickers(accountId) + .then(stickers => setStickers(stickers)) + }, [accountId]) useLayoutEffect(() => { document @@ -132,7 +144,7 @@ export const EmojiAndStickerPicker = forwardRef< return (
diff --git a/src/renderer/components/dialogs/ChatAuditLogDialog.tsx b/src/renderer/components/dialogs/ChatAuditLogDialog.tsx index 7fb424689b..5c2411611f 100644 --- a/src/renderer/components/dialogs/ChatAuditLogDialog.tsx +++ b/src/renderer/components/dialogs/ChatAuditLogDialog.tsx @@ -102,7 +102,7 @@ export default function ChatAuditLogDialog(props: { setLoading(true) const account_id = selectedAccountId() - const msgIds = await BackendRemote.rpc.messageListGetMessageIds( + const msgIds = await BackendRemote.rpc.getMessageIds( account_id, selectedChat.id, C.DC_GCM_ADDDAYMARKER | C.DC_GCM_INFO_ONLY diff --git a/src/renderer/components/dialogs/CreateChat.tsx b/src/renderer/components/dialogs/CreateChat.tsx index e32985fc6a..fa073a5517 100644 --- a/src/renderer/components/dialogs/CreateChat.tsx +++ b/src/renderer/components/dialogs/CreateChat.tsx @@ -11,7 +11,6 @@ import React, { import { Card, Classes } from '@blueprintjs/core' import { C } from 'deltachat-node/node/dist/constants' -import { DeltaBackend } from '../../delta-remote' import { ScreenContext, useTranslationFunction } from '../../contexts' import { useContacts, @@ -385,7 +384,7 @@ export function AddMemberInnerDialog({ const _onCancel = async () => { for (const contactId of contactsToDeleteOnCancel) { - await DeltaBackend.call('contacts.deleteContact', contactId) + await BackendRemote.rpc.deleteContact(selectedAccountId(), contactId) } onCancel() } @@ -572,22 +571,27 @@ const useCreateGroup = ( onClose: DialogProps['onClose'] ) => { const [groupId, setGroupId] = useState(-1) + const accountId = selectedAccountId() const lazilyCreateOrUpdateGroup = async (finishing: boolean) => { let gId = groupId if (gId === -1) { - gId = await DeltaBackend.call('chat.createGroupChat', verified, groupName) + gId = await BackendRemote.rpc.createGroupChat( + accountId, + groupName, + verified + ) setGroupId(gId) } else { - await DeltaBackend.call('chat.setName', gId, groupName) + await BackendRemote.rpc.setChatName(accountId, gId, groupName) } if (finishing === true) { if (groupImage && groupImage !== '') { - await DeltaBackend.call('chat.setProfileImage', gId, groupImage) + await BackendRemote.rpc.setChatProfileImage(accountId, gId, groupImage) } for (const contactId of groupMembers) { if (contactId !== C.DC_CONTACT_ID_SELF) { - await DeltaBackend.call('chat.addContactToChat', gId, contactId) + await BackendRemote.rpc.addContactToChat(accountId, gId, contactId) } } } @@ -762,17 +766,18 @@ const useCreateBroadcast = ( onClose: DialogProps['onClose'] ) => { const [broadcastId, setBroadcastId] = useState(-1) + const accountId = selectedAccountId() const lazilyCreateOrUpdateBroadcast = async (finishing: boolean) => { let bId = broadcastId if (bId === -1) { - bId = await DeltaBackend.call('chat.createBroadcastList') + bId = await BackendRemote.rpc.createBroadcastList(accountId) setBroadcastId(bId) } if (finishing === true) { for (const contactId of broadcastRecipients) { if (contactId !== C.DC_CONTACT_ID_SELF) { - await DeltaBackend.call('chat.addContactToChat', bId, contactId) + await BackendRemote.rpc.addContactToChat(accountId, bId, contactId) } } } diff --git a/src/renderer/components/dialogs/DisappearingMessages.tsx b/src/renderer/components/dialogs/DisappearingMessages.tsx index 4aad5ec838..bc8758bcf6 100644 --- a/src/renderer/components/dialogs/DisappearingMessages.tsx +++ b/src/renderer/components/dialogs/DisappearingMessages.tsx @@ -8,9 +8,10 @@ import { DeltaDialogFooterActions, } from './DeltaDialog' import { RadioGroup, Radio } from '@blueprintjs/core' -import { DeltaBackend } from '../../delta-remote' import { Timespans } from '../../../shared/constants' import { useTranslationFunction } from '../../contexts' +import { BackendRemote } from '../../backend-com' +import { selectedAccountId } from '../../ScreenController' enum DisappearingMessageDuration { OFF = Timespans.ZERO_SECONDS, @@ -99,8 +100,8 @@ export default function DisappearingMessage({ useEffect(() => { ;(async () => { - const ephemeralTimer = await DeltaBackend.call( - 'chat.getChatEphemeralTimer', + const ephemeralTimer = await BackendRemote.rpc.getChatEphemeralTimer( + selectedAccountId(), chatId ) setDisappearingMessageDuration(ephemeralTimer) @@ -109,8 +110,8 @@ export default function DisappearingMessage({ }, [chatId]) const saveAndClose = async () => { - await DeltaBackend.call( - 'chat.setChatEphemeralTimer', + await BackendRemote.rpc.setChatEphemeralTimer( + selectedAccountId(), chatId, disappearingMessageDuration ) diff --git a/src/renderer/components/dialogs/MessageListProfile.tsx b/src/renderer/components/dialogs/MessageListProfile.tsx index 519967c987..da51788074 100644 --- a/src/renderer/components/dialogs/MessageListProfile.tsx +++ b/src/renderer/components/dialogs/MessageListProfile.tsx @@ -1,5 +1,4 @@ import React, { useState } from 'react' -import { DeltaBackend } from '../../delta-remote' import { Card, Classes } from '@blueprintjs/core' import { DeltaDialogBase, @@ -13,6 +12,7 @@ import { import { DialogProps } from './DialogController' import { Type } from '../../backend-com' +import { modifyGroup } from '../helpers/ChatMethods' export default function MailingListProfile(props: { isOpen: DialogProps['isOpen'] @@ -63,13 +63,7 @@ const useEdit = ( onClose: DialogProps['onClose'] ) => { const updateGroup = async () => { - await DeltaBackend.call( - 'chat.modifyGroup', - groupId, - groupName, - groupImage || undefined, - null - ) + await modifyGroup(groupId, groupName, groupImage || undefined, null) } const onUpdateGroup = async () => { if (groupName === '') return diff --git a/src/renderer/components/dialogs/Settings-Appearance.tsx b/src/renderer/components/dialogs/Settings-Appearance.tsx index 48ba7a1375..df76defd81 100644 --- a/src/renderer/components/dialogs/Settings-Appearance.tsx +++ b/src/renderer/components/dialogs/Settings-Appearance.tsx @@ -1,7 +1,6 @@ import { ScreenContext, useTranslationFunction } from '../../contexts' import React, { useContext, useEffect, useState } from 'react' import { H6, Icon } from '@blueprintjs/core' -import { DeltaBackend } from '../../delta-remote' import { ThemeManager } from '../../ThemeManager' import { SettingsSelector } from './Settings' import { SmallSelectDialog, SelectDialogOption } from './DeltaDialog' @@ -13,6 +12,9 @@ import { } from '../../../shared/shared-types' import { join } from 'path' import SettingsStoreInstance from '../../stores/settings' +import { getLogger } from '../../../shared/logger' + +const log = getLogger('renderer/settings/appearance') const enum SetBackgroundAction { default, @@ -83,14 +85,13 @@ function BackgroundSelector({ } SettingsStoreInstance.effect.setDesktopSetting( 'chatViewBgImg', - await DeltaBackend.call('settings.saveBackgroundImage', url, false) + await runtime.saveBackgroundImage(url, false) ) break case SetBackgroundAction.presetImage: SettingsStoreInstance.effect.setDesktopSetting( 'chatViewBgImg', - await DeltaBackend.call( - 'settings.saveBackgroundImage', + await runtime.saveBackgroundImage( (ev.target as any).dataset.url, true ) @@ -198,9 +199,7 @@ export default function SettingsAppearance({ const [availableThemes, setAvailableThemes] = useState([]) useEffect(() => { ;(async () => { - const availableThemes = await DeltaBackend.call( - 'extras.getAvailableThemes' - ) + const availableThemes = await runtime.getAvailableThemes() setAvailableThemes( availableThemes.filter( @@ -211,7 +210,7 @@ export default function SettingsAppearance({ }, [rc.devmode, activeTheme]) const setTheme = async (theme: string) => { - if (await DeltaBackend.call('extras.setTheme', theme)) { + if (await setThemeFunction(theme)) { SettingsStoreInstance.effect.setDesktopSetting('activeTheme', theme) await ThemeManager.refresh() } @@ -282,3 +281,14 @@ export default function SettingsAppearance({ ) } + +async function setThemeFunction(address: string) { + try { + runtime.resolveThemeAddress(address) + await runtime.setDesktopSetting('activeTheme', address) + return true + } catch (error) { + log.error('set theme failed: ', error) + return false + } +} diff --git a/src/renderer/components/dialogs/Settings-Autodelete.tsx b/src/renderer/components/dialogs/Settings-Autodelete.tsx index 580aaacf0c..d872d48184 100644 --- a/src/renderer/components/dialogs/Settings-Autodelete.tsx +++ b/src/renderer/components/dialogs/Settings-Autodelete.tsx @@ -11,7 +11,6 @@ import { SelectDialogOption, } from './DeltaDialog' import { DialogProps } from './DialogController' -import { DeltaBackend } from '../../delta-remote' import { SettingsSelector } from './Settings' import { AutodeleteDuration } from '../../../shared/constants' import { DeltaCheckbox } from '../contact/ContactListItem' @@ -19,6 +18,8 @@ import classNames from 'classnames' import SettingsStoreInstance, { SettingsStoreState, } from '../../stores/settings' +import { BackendRemote } from '../../backend-com' +import { selectedAccountId } from '../../ScreenController' function durationToString(configValue: number | string) { if (typeof configValue === 'string') configValue = Number(configValue) @@ -128,6 +129,7 @@ export default function SettingsAutodelete({ settingsStore: SettingsStoreState }) { const { openDialog } = useContext(ScreenContext) + const accountId = selectedAccountId() const tx = useTranslationFunction() @@ -165,8 +167,8 @@ export default function SettingsAutodelete({ : tx('autodel_device_title'), onSave: async (_seconds: string) => { const seconds = Number(_seconds) - const estimateCount = await DeltaBackend.call( - 'settings.estimateAutodeleteCount', + const estimateCount = await BackendRemote.rpc.estimateAutoDeletionCount( + accountId, fromServer, seconds ) diff --git a/src/renderer/components/dialogs/Settings-Backup.tsx b/src/renderer/components/dialogs/Settings-Backup.tsx index fc6a91877e..6ff5b52644 100644 --- a/src/renderer/components/dialogs/Settings-Backup.tsx +++ b/src/renderer/components/dialogs/Settings-Backup.tsx @@ -2,30 +2,32 @@ import React, { useState, useEffect } from 'react' import { H5, Intent } from '@blueprintjs/core' import { SettingsButton } from './Settings' import type { OpenDialogOptions } from 'electron' -import { ipcBackend } from '../../ipc' import { DialogProps } from './DialogController' import { DeltaDialogBody, DeltaDialogContent, SmallDialog } from './DeltaDialog' import { DeltaProgressBar } from '../Login-Styles' -import { DeltaBackend } from '../../delta-remote' import { useTranslationFunction } from '../../contexts' import { runtime } from '../../runtime' import { getLogger } from '../../../shared/logger' +import { BackendRemote } from '../../backend-com' +import { selectedAccountId } from '../../ScreenController' +import { DcEventType } from '@deltachat/jsonrpc-client' const log = getLogger('renderer/Settings/Backup') function ExportProgressDialog(props: DialogProps) { const tx = useTranslationFunction() const [progress, setProgress] = useState(0.0) - const onImexProgress = (_: any, [progress, _data2]: [number, any]) => { + const onImexProgress = ({ progress }: DcEventType<'ImexProgress'>) => { setProgress(progress) } + const accountId = selectedAccountId() useEffect(() => { - ipcBackend.on('DC_EVENT_IMEX_PROGRESS', onImexProgress) - + const emitter = BackendRemote.getContextEvents(accountId) + emitter.on('ImexProgress', onImexProgress) return () => { - ipcBackend.removeListener('DC_EVENT_IMEX_PROGRESS', onImexProgress) + emitter.off('ImexProgress', onImexProgress) } - }, []) + }, [accountId]) return ( {}}> @@ -42,6 +44,7 @@ function ExportProgressDialog(props: DialogProps) { } function onBackupExport() { + const accountId = selectedAccountId() const tx = window.static_translate const openDialog = window.__openDialog @@ -67,28 +70,25 @@ function onBackupExport() { return } - const listenForOutputFile = ( - _event: any, - [_, filename]: [any, string] - ) => { + const listenForOutputFile = ({ + path: filename, + }: DcEventType<'ImexFileWritten'>) => { userFeedback({ type: 'success', text: tx('pref_backup_written_to_x', filename), }) } - ipcBackend.once('DC_EVENT_IMEX_FILE_WRITTEN', listenForOutputFile) + const emitter = BackendRemote.getContextEvents(selectedAccountId()) + emitter.once('ImexFileWritten', listenForOutputFile) const dialog_number = openDialog(ExportProgressDialog) try { - await DeltaBackend.call('backup.export', destination) + await BackendRemote.rpc.exportBackup(accountId, destination, null) } catch (error) { // TODO/QUESTION - how are errors shown to user? log.error('backup-export failed:', error) } finally { - ipcBackend.removeListener( - 'DC_EVENT_IMEX_FILE_WRITTEN', - listenForOutputFile - ) + emitter.off('ImexFileWritten', listenForOutputFile) closeDialog(dialog_number) } }, diff --git a/src/renderer/components/dialogs/Settings-Connectivity.tsx b/src/renderer/components/dialogs/Settings-Connectivity.tsx index 5a94bb4156..9ae6d9f784 100644 --- a/src/renderer/components/dialogs/Settings-Connectivity.tsx +++ b/src/renderer/components/dialogs/Settings-Connectivity.tsx @@ -8,11 +8,10 @@ import { DeltaDialogBase, DeltaDialogHeader, } from './DeltaDialog' -import { onDCEvent } from '../../ipc' import { debounceWithInit } from '../chat/ChatListHelpers' import { DialogProps } from './DialogController' import { useTranslationFunction } from '../../contexts' -import { BackendRemote } from '../../backend-com' +import { BackendRemote, onDCEvent } from '../../backend-com' import { selectedAccountId } from '../../ScreenController' const INHERIT_STYLES = ['line-height', 'background-color', 'color', 'font-size'] @@ -65,6 +64,7 @@ export async function getConnectivityHTML( } export function SettingsConnectivityInner() { + const accountId = selectedAccountId() const [connectivityHTML, setConnectivityHTML] = useState('') const styleSensor = useRef(null) @@ -79,8 +79,8 @@ export function SettingsConnectivityInner() { useEffect(() => { updateConnectivity() - return onDCEvent('DC_EVENT_CONNECTIVITY_CHANGED', updateConnectivity) - }) + return onDCEvent(accountId, 'ConnectivityChanged', updateConnectivity) + }, [accountId, updateConnectivity]) return ( <> diff --git a/src/renderer/components/dialogs/Settings-ManageKeys.tsx b/src/renderer/components/dialogs/Settings-ManageKeys.tsx index dde4ed7129..dc9dd56f5c 100644 --- a/src/renderer/components/dialogs/Settings-ManageKeys.tsx +++ b/src/renderer/components/dialogs/Settings-ManageKeys.tsx @@ -2,9 +2,10 @@ import React from 'react' import { H5 } from '@blueprintjs/core' import { SettingsButton } from './Settings' import type { OpenDialogOptions } from 'electron' -import { DeltaBackend } from '../../delta-remote' -import { ipcBackend } from '../../ipc' import { runtime } from '../../runtime' +import { BackendRemote } from '../../backend-com' +import { selectedAccountId } from '../../ScreenController' +import { DcEventType } from '@deltachat/jsonrpc-client' async function onKeysImport() { const tx = window.static_translate @@ -29,13 +30,19 @@ async function onKeysImport() { return } const text = tx('pref_managekeys_secret_keys_imported_from_x', filename) - ipcBackend.on('DC_EVENT_IMEX_PROGRESS', (_event, progress) => { + const onImexProgress = ({ progress }: DcEventType<'ImexProgress'>) => { if (progress !== 1000) { return } window.__userFeedback({ type: 'success', text }) - }) - DeltaBackend.call('settings.keysImport', filename) + } + const emitter = BackendRemote.getContextEvents(selectedAccountId()) + emitter.on('ImexProgress', onImexProgress) + BackendRemote.rpc + .importSelfKeys(selectedAccountId(), filename, null) + .finally(() => { + emitter.off('ImexProgress', onImexProgress) + }) }, }) } @@ -63,17 +70,19 @@ async function onKeysExport() { message: title, confirmLabel: tx('yes'), cancelLabel: tx('no'), - cb: (yes: boolean) => { + cb: async (yes: boolean) => { if (!yes || !destination) { return } - ipcBackend.once('DC_EVENT_IMEX_FILE_WRITTEN', (_event, filename) => { - window.__userFeedback({ - type: 'success', - text: tx('pref_managekeys_secret_keys_exported_to_x', filename), - }) + await BackendRemote.rpc.exportSelfKeys( + selectedAccountId(), + destination, + null + ) + window.__userFeedback({ + type: 'success', + text: tx('pref_managekeys_secret_keys_exported_to_x', destination), }) - DeltaBackend.call('settings.keysExport', destination) }, }) } diff --git a/src/renderer/components/dialogs/Settings-Profile.tsx b/src/renderer/components/dialogs/Settings-Profile.tsx index 63906ff43c..d13b5989c9 100644 --- a/src/renderer/components/dialogs/Settings-Profile.tsx +++ b/src/renderer/components/dialogs/Settings-Profile.tsx @@ -2,7 +2,6 @@ import { Card, Elevation } from '@blueprintjs/core' import React, { useEffect, useState, useContext, useCallback } from 'react' import { useTranslationFunction, ScreenContext } from '../../contexts' -import { DeltaBackend } from '../../delta-remote' import { avatarInitial, ClickForFullscreenAvatarWrapper } from '../Avatar' import { DeltaInput, DeltaTextarea } from '../Login-Styles' import { @@ -15,13 +14,12 @@ import { SettingsButton } from './Settings' import { runtime } from '../../runtime' import { C } from 'deltachat-node/node/dist/constants' import { DialogProps } from './DialogController' -import { onDCEvent } from '../../ipc' import SettingsAccountDialog from './Settings-Account' import SettingsConnectivityDialog from './Settings-Connectivity' import SettingsStoreInstance, { SettingsStoreState, } from '../../stores/settings' -import { BackendRemote } from '../../backend-com' +import { BackendRemote, onDCEvent } from '../../backend-com' import { selectedAccountId } from '../../ScreenController' export default function SettingsProfile({ @@ -60,9 +58,8 @@ export default function SettingsProfile({ ) useEffect(() => { updateConnectivity() - - return onDCEvent('DC_EVENT_CONNECTIVITY_CHANGED', updateConnectivity) - }, [updateConnectivity]) + return onDCEvent(accountId, 'ConnectivityChanged', updateConnectivity) + }, [updateConnectivity, accountId]) const tx = useTranslationFunction() const profileBlobUrl = runtime.transformBlobURL( @@ -206,8 +203,9 @@ export function SettingsEditProfileDialogInner({ onClose() } const onOk = async () => { - await DeltaBackend.call( - 'setProfilePicture', + await BackendRemote.rpc.setConfig( + selectedAccountId(), + 'selfavatar', profilePicture ? profilePicture : null ) SettingsStoreInstance.effect.setCoreSetting('displayname', displayname) diff --git a/src/renderer/components/dialogs/Settings-Webxdc.tsx b/src/renderer/components/dialogs/Settings-Webxdc.tsx index cdcf79057b..e57ae360cb 100644 --- a/src/renderer/components/dialogs/Settings-Webxdc.tsx +++ b/src/renderer/components/dialogs/Settings-Webxdc.tsx @@ -3,19 +3,20 @@ import { Card, Elevation, H5 } from '@blueprintjs/core' import filesizeConverter from 'filesize' import { ScreenContext } from '../../contexts' -import { DeltaBackend } from '../../delta-remote' import ConfirmationDialog from './ConfirmationDialog' import { runtime } from '../../runtime' +import { selectedAccountId } from '../../ScreenController' export default function SettingsWebxdc() { + const accountId = selectedAccountId() const [usage, setUsage] = useState<{ total_size: number data_size: number } | null>(null) const updateUsage = useCallback(() => { - DeltaBackend.call('webxdc.getWebxdcDiskUsage').then(setUsage) - }, []) + runtime.getWebxdcDiskUsage(accountId).then(setUsage) + }, [accountId]) useEffect(() => updateUsage(), [updateUsage]) @@ -28,8 +29,7 @@ export default function SettingsWebxdc() { "Delete all webxdc DOMStorage data, if you do that you might loose some local settings of your webxdc's", confirmLabel: tx('delete'), cb: yes => - yes && - DeltaBackend.call('webxdc.clearWebxdcDOMStorage').then(updateUsage), + yes && runtime.clearWebxdcDOMStorage(accountId).then(updateUsage), }) } diff --git a/src/renderer/components/dialogs/Settings.tsx b/src/renderer/components/dialogs/Settings.tsx index 0cea75373a..b1d8cb1c6c 100644 --- a/src/renderer/components/dialogs/Settings.tsx +++ b/src/renderer/components/dialogs/Settings.tsx @@ -1,7 +1,6 @@ import React, { useState, useEffect } from 'react' import { Elevation, Card } from '@blueprintjs/core' -const { ipcRenderer } = window.electron_functions import { useTranslationFunction } from '../../contexts' import { DesktopSettingsType } from '../../../shared/shared-types' @@ -359,12 +358,6 @@ export default function Settings(props: DialogProps) { ) } - useEffect(() => { - return () => { - ipcRenderer.removeAllListeners('DC_EVENT_IMEX_FILE_WRITTEN') - } - }, []) - const { onClose } = props return ( diff --git a/src/renderer/components/dialogs/UnblockContacts.tsx b/src/renderer/components/dialogs/UnblockContacts.tsx index 9a7e7e814c..cae193d158 100644 --- a/src/renderer/components/dialogs/UnblockContacts.tsx +++ b/src/renderer/components/dialogs/UnblockContacts.tsx @@ -4,9 +4,8 @@ import DeltaDialog, { DeltaDialogBody, DeltaDialogContent } from './DeltaDialog' import { ContactList2 } from '../contact/ContactList' import { ScreenContext } from '../../contexts' import { DialogProps } from './DialogController' -import { onDCEvent } from '../../ipc' import debounce from 'debounce' -import { BackendRemote, Type } from '../../backend-com' +import { BackendRemote, onDCEvent, Type } from '../../backend-com' import { selectedAccountId } from '../../ScreenController' export default function UnblockContacts(props: { @@ -26,7 +25,8 @@ export default function UnblockContacts(props: { } onContactsUpdate() return onDCEvent( - 'DC_EVENT_CONTACTS_CHANGED', + accountId, + 'ContactsChanged', debounce(onContactsUpdate, 500) ) }, [accountId]) diff --git a/src/renderer/components/dialogs/ViewGroup.tsx b/src/renderer/components/dialogs/ViewGroup.tsx index 410769b4a5..5362a049ba 100644 --- a/src/renderer/components/dialogs/ViewGroup.tsx +++ b/src/renderer/components/dialogs/ViewGroup.tsx @@ -1,4 +1,3 @@ -import { DeltaBackend } from '../../delta-remote' import { C } from 'deltachat-node/node/dist/constants' import { Card, Classes, Elevation } from '@blueprintjs/core' import { @@ -27,10 +26,11 @@ import { } from '../Avatar' import { runtime } from '../../runtime' import { DeltaInput } from '../Login-Styles' -import { ipcBackend } from '../../ipc' import { getLogger } from '../../../shared/logger' import { BackendRemote, Type } from '../../backend-com' import { selectedAccountId } from '../../ScreenController' +import { modifyGroup } from '../helpers/ChatMethods' +import { DcEventType } from '@deltachat/jsonrpc-client' const log = getLogger('renderer/ViewGroup') @@ -52,7 +52,7 @@ export function useChat(initialChat: Type.FullChat): Type.FullChat { }, [initialChat.id, accountId]) const onChatModified = useMemo( - () => async (_: any, [chatId, _2]: [number, number]) => { + () => async ({ chatId }: DcEventType<'ChatModified'>) => { if (chatId !== chat.id) return updateChat() }, @@ -63,11 +63,12 @@ export function useChat(initialChat: Type.FullChat): Type.FullChat { updateChat() }, [initialChat, updateChat]) useEffect(() => { - ipcBackend.on('DC_EVENT_CHAT_MODIFIED', onChatModified) + const emitter = BackendRemote.getContextEvents(accountId) + emitter.on('ChatModified', onChatModified) return () => { - ipcBackend.removeListener('DC_EVENT_CHAT_MODIFIED', onChatModified) + emitter.on('ChatModified', onChatModified) } - }, [onChatModified]) + }, [onChatModified, accountId]) return chat } @@ -112,13 +113,7 @@ export const useGroup = (chat: Type.FullChat) => { useEffect(() => { ;(async () => { - DeltaBackend.call( - 'chat.modifyGroup', - chat.id, - groupName, - groupImage || undefined, - groupMembers - ) + modifyGroup(chat.id, groupName, groupImage || undefined, groupMembers) })() }, [groupName, groupImage, groupMembers, chat.id]) diff --git a/src/renderer/components/dialogs/ViewProfile.tsx b/src/renderer/components/dialogs/ViewProfile.tsx index b5167c8bf2..13d903b26c 100644 --- a/src/renderer/components/dialogs/ViewProfile.tsx +++ b/src/renderer/components/dialogs/ViewProfile.tsx @@ -1,4 +1,4 @@ -import React, { useState, useContext } from 'react' +import React, { useState, useContext, useEffect } from 'react' import { DeltaDialogBase, DeltaDialogHeader, @@ -9,7 +9,6 @@ import { } from './DeltaDialog' import ChatListItem from '../chat/ChatListItem' import { useChatList } from '../chat/ChatListHelpers' -import { DeltaBackend } from '../../delta-remote' import { C } from 'deltachat-node/node/dist/constants' import { MessagesDisplayContext, @@ -25,7 +24,7 @@ import { DialogProps } from './DialogController' import { Card, Elevation } from '@blueprintjs/core' import { DeltaInput } from '../Login-Styles' import { selectChat } from '../helpers/ChatMethods' -import { BackendRemote, Type } from '../../backend-com' +import { BackendRemote, onDCEvent, Type } from '../../backend-com' import { selectedAccountId } from '../../ScreenController' import moment from 'moment' @@ -86,6 +85,7 @@ export default function ViewProfile(props: { }) { const { isOpen, onClose } = props + const accountId = selectedAccountId() const tx = window.static_translate const { openDialog } = useContext(ScreenContext) const [contact, setContact] = useState(props.contact) @@ -95,16 +95,23 @@ export default function ViewProfile(props: { openDialog(EditContactNameDialog, { contactName: contact.name, onOk: async (contactName: string) => { - await DeltaBackend.call( - 'contacts.changeNickname', + await BackendRemote.rpc.changeContactName( + accountId, contact.id, contactName ) - setContact({ ...contact, name: contactName }) }, }) } + useEffect(() => { + return onDCEvent(accountId, 'ContactsChanged', () => { + BackendRemote.rpc + .contactsGetContact(accountId, contact.id) + .then(setContact) + }) + }, [accountId, contact.id]) + return ( { export async function setChatVisibility( chatId: number, - visibility: - | C.DC_CHAT_VISIBILITY_NORMAL - | C.DC_CHAT_VISIBILITY_ARCHIVED - | C.DC_CHAT_VISIBILITY_PINNED, + visibility: T.ChatVisibility, shouldUnselectChat = false ) { - await DeltaBackend.call('chat.setVisibility', chatId, visibility) + await BackendRemote.rpc.setChatVisibility( + selectedAccountId(), + chatId, + visibility + ) if (shouldUnselectChat) unselectChat() } @@ -59,7 +61,8 @@ export function openLeaveChatDialog( confirmLabel: tx('menu_leave_group'), isConfirmDanger: true, noMargin: true, - cb: (yes: boolean) => yes && DeltaBackend.call('chat.leaveGroup', chatId), + cb: (yes: boolean) => + yes && BackendRemote.rpc.leaveGroup(selectedAccountId(), chatId), }) } @@ -149,7 +152,6 @@ export async function openMuteChatDialog( screenContext: unwrapContext, chatId: number ) { - // todo open dialog to ask for duration screenContext.openDialog('MuteChat', { chatId }) } @@ -166,8 +168,8 @@ export async function sendCallInvitation( chatId: number ) { try { - const messageId = await DeltaBackend.call( - 'chat.sendVideoChatInvitation', + const messageId = await BackendRemote.rpc.sendVideochatInvitation( + selectedAccountId(), chatId ) ChatStore.effect.jumpToMessage(messageId, false) @@ -226,10 +228,10 @@ export async function createChatByContactIdAndSelectIt( if (chat && chat.archived) { log.debug('chat was archived, unarchiving it') - await DeltaBackend.call( - 'chat.setVisibility', + await BackendRemote.rpc.setChatVisibility( + selectedAccountId(), chatId, - C.DC_CHAT_VISIBILITY_NORMAL + 'Normal' ) } @@ -249,7 +251,7 @@ export const deleteMessage = (messageId: number) => { export async function clearChat(chatId: number) { const accountID = selectedAccountId() const tx = window.static_translate - const messages_to_delete = await BackendRemote.rpc.messageListGetMessageIds( + const messages_to_delete = await BackendRemote.rpc.getMessageIds( accountID, chatId, 0 @@ -267,3 +269,43 @@ export async function clearChat(chatId: number) { }, }) } + +export async function modifyGroup( + chatId: number, + name: string, + image: string | undefined, + members: number[] | null +) { + const accountId = selectedAccountId() + log.debug('action - modify group', { chatId, name, image, members }) + await BackendRemote.rpc.setChatName(accountId, chatId, name) + const chat = await BackendRemote.rpc.chatlistGetFullChatById( + accountId, + chatId + ) + if (!chat) { + throw new Error('chat is undefined, this should not happen') + } + if (typeof image !== 'undefined' && chat.profileImage !== image) { + await BackendRemote.rpc.setChatProfileImage( + accountId, + chatId, + image || null + ) + } + + if (members !== null) { + const previousMembers = [...chat.contactIds] + const remove = previousMembers.filter(m => !members.includes(m)) + const add = members.filter(m => !previousMembers.includes(m)) + + await Promise.all( + remove.map(id => + BackendRemote.rpc.removeContactFromChat(accountId, chatId, id) + ) + ) + await Promise.all( + add.map(id => BackendRemote.rpc.addContactToChat(accountId, chatId, id)) + ) + } +} diff --git a/src/renderer/components/helpers/OpenQrUrl.tsx b/src/renderer/components/helpers/OpenQrUrl.tsx index 5fad587223..18a25da257 100644 --- a/src/renderer/components/helpers/OpenQrUrl.tsx +++ b/src/renderer/components/helpers/OpenQrUrl.tsx @@ -1,5 +1,4 @@ import React from 'react' -import { DeltaBackend } from '../../delta-remote' import { ConfigureProgressDialog } from '../LoginForm' import { Screens, selectedAccountId } from '../../ScreenController' import { useTranslationFunction } from '../../contexts' @@ -89,11 +88,11 @@ export default async function processOpenQrUrl( : mailto.body if (mailto.to) { - let contactId = await DeltaBackend.call( - 'contacts.lookupContactIdByAddr', + let contactId = await BackendRemote.rpc.lookupContactIdByAddr( + accountId, mailto.to ) - if (contactId == 0) { + if (contactId === null) { contactId = await BackendRemote.rpc.contactsCreateContact( accountId, mailto.to, @@ -257,20 +256,21 @@ export default async function processOpenQrUrl( window.__openDialog('ConfirmationDialog', { message: tx('ask_start_chat_with', contact.address), confirmLabel: tx('ok'), - cb: async (confirmed: boolean) => { + cb: (confirmed: boolean) => { if (confirmed) { - DeltaBackend.call('joinSecurejoin', url).then(callback) + BackendRemote.rpc.secureJoin(accountId, url).then(callback) } }, }) } else if (checkQr.type === 'askVerifyGroup') { + const accountId = selectedAccountId() closeProcessDialog() window.__openDialog('ConfirmationDialog', { message: tx('qrscan_ask_join_group', checkQr.grpname), confirmLabel: tx('ok'), cb: (confirmed: boolean) => { if (confirmed) { - DeltaBackend.call('joinSecurejoin', url).then(callback) + BackendRemote.rpc.secureJoin(accountId, url).then(callback) } return }, diff --git a/src/renderer/components/map/MapComponent.tsx b/src/renderer/components/map/MapComponent.tsx index 44d00a0436..5354a689cf 100644 --- a/src/renderer/components/map/MapComponent.tsx +++ b/src/renderer/components/map/MapComponent.tsx @@ -18,9 +18,9 @@ import chatStore from '../../stores/chat' import { state as LocationStoreState } from '../../stores/locations' import ContextMenu from './ContextMenu' -import { JsonLocations } from '../../../shared/shared-types' import { BackendRemote, Type } from '../../backend-com' import { selectedAccountId } from '../../ScreenController' +import { T } from '@deltachat/jsonrpc-client' type MapData = { contact: Type.Contact @@ -234,7 +234,7 @@ export default class MapComponent extends React.Component< poiWithMarker.map(location => { const el = document.createElement('div') el.className = 'marker-icon' - el.innerHTML = location.marker + el.innerText = location.marker || '?' // make a marker for each feature and add to the map const m = new mapboxgl.Marker(el) .setLngLat([location.longitude, location.latitude]) @@ -259,7 +259,7 @@ export default class MapComponent extends React.Component< } } - renderContactLayer(contact: Contact, locationsForContact: JsonLocations) { + renderContactLayer(contact: Contact, locationsForContact: T.Location[]) { if (!this.map) { throw new Error('this.map is unset') } @@ -348,7 +348,7 @@ export default class MapComponent extends React.Component< addOrUpdateLayerForContact( mapData: MapData, - locationsForContact: JsonLocations + locationsForContact: T.Location[] ) { if (!this.map) { throw new Error('this.map is unset') @@ -376,7 +376,7 @@ export default class MapComponent extends React.Component< } } - addPathLayer(locationsForContact: JsonLocations, mapData: MapData) { + addPathLayer(locationsForContact: T.Location[], mapData: MapData) { if (!this.map) { throw new Error('this.map is unset') } @@ -397,7 +397,7 @@ export default class MapComponent extends React.Component< } } - addPathJointsLayer(locationsForContact: JsonLocations, data: MapData) { + addPathJointsLayer(locationsForContact: T.Location[], data: MapData) { if (!this.map) { throw new Error('this.map is unset') } diff --git a/src/renderer/components/map/MapLayerFactory.ts b/src/renderer/components/map/MapLayerFactory.ts index d4ee37f4f2..2419417703 100644 --- a/src/renderer/components/map/MapLayerFactory.ts +++ b/src/renderer/components/map/MapLayerFactory.ts @@ -1,4 +1,4 @@ -import { JsonLocations } from '../../../shared/shared-types' +import { T } from '@deltachat/jsonrpc-client' import { Type } from '../../backend-com' // todo: get this from some settings/config file @@ -7,7 +7,7 @@ const accessToken = export default class MapLayerFactory { static getGeoJSONLineSourceData( - locations: JsonLocations + locations: T.Location[] ): mapboxgl.GeoJSONSourceOptions['data'] { const coordinates = locations.map(point => [ point.longitude, @@ -64,7 +64,7 @@ export default class MapLayerFactory { } static getGeoJSONPointsLayerSourceData( - locations: JsonLocations, + locations: T.Location[], contact: Type.Contact, withMessageOnly: boolean ): mapboxgl.GeoJSONSourceOptions['data'] { @@ -93,7 +93,7 @@ export default class MapLayerFactory { } } - static getPOILayer(locations: JsonLocations) { + static getPOILayer(locations: T.Location[]) { const layer: mapboxgl.Layer = { id: 'poi-layer', type: 'symbol', diff --git a/src/renderer/components/message/MessageList.tsx b/src/renderer/components/message/MessageList.tsx index 7728036ec5..8eda9cd89f 100644 --- a/src/renderer/components/message/MessageList.tsx +++ b/src/renderer/components/message/MessageList.tsx @@ -21,9 +21,7 @@ import moment from 'moment' import { getLogger } from '../../../shared/logger' import { MessagesDisplayContext, useTranslationFunction } from '../../contexts' import { KeybindAction, useKeyBindingAction } from '../../keybindings' -import { BackendRemote } from '../../backend-com' -import { selectedAccountId } from '../../ScreenController' -import { debouncedUpdateBadgeCounter } from '../../system-integration/badge-counter' +import { T } from '@deltachat/jsonrpc-client' const log = getLogger('render/components/message/MessageList') window.addEventListener('focus', () => { @@ -52,9 +50,9 @@ window.addEventListener('focus', () => { `window was focused: marking ${messageIdsToMarkAsRead.length} visible messages as read`, messageIdsToMarkAsRead ) - BackendRemote.rpc - .markseenMsgs(selectedAccountId(), messageIdsToMarkAsRead) - .then(debouncedUpdateBadgeCounter) + const chatId = ChatStore.state.chat?.id + if (!chatId) return + ChatStore.effect.markseenMessages(chatId, messageIdsToMarkAsRead) } }) @@ -68,9 +66,8 @@ export default function MessageList({ const { oldestFetchedMessageIndex, messagePages, - messageIds, - scrollTo, - lastKnownScrollHeight, + messageListItems, + viewState, } = useChatStore() const messageListRef = useRef(null) const [showJumpDownButton, setShowJumpDownButton] = useState(false) @@ -96,6 +93,11 @@ export default function MessageList({ // Don't mark messages as read if window is not focused if (document.hasFocus() === false) return + if (ChatStore.scheduler.isLocked('scroll') === true) { + //console.log('onScroll: locked, returning') + return + } + setTimeout(() => { log.debug(`onUnreadMessageInView: entries.length: ${entries.length}`) @@ -120,9 +122,9 @@ export default function MessageList({ } if (messageIdsToMarkAsRead.length > 0) { - BackendRemote.rpc - .markseenMsgs(selectedAccountId(), messageIdsToMarkAsRead) - .then(debouncedUpdateBadgeCounter) + const chatId = ChatStore.state.chat?.id + if (!chatId) return + ChatStore.effect.markseenMessages(chatId, messageIdsToMarkAsRead) } }) } @@ -147,7 +149,7 @@ export default function MessageList({ if (!messageListRef.current) { return } - if (ChatStore.lockIsLocked('scroll') === true) { + if (ChatStore.scheduler.isLocked('scroll') === true) { //console.log('onScroll: locked, returning') return } @@ -158,8 +160,8 @@ export default function MessageList({ messageListRef.current.clientHeight const isNewestMessageLoaded = - ChatStore.state.newestFetchedMessageIndex === - ChatStore.state.messageIds.length - 1 + ChatStore.state.newestFetchedMessageListItemIndex === + ChatStore.state.messageListItems.length - 1 const onePageAwayFromNewestMessageTreshold = messageListRef.current.clientHeight / 3 const newShowJumpDownButton = @@ -202,10 +204,12 @@ export default function MessageList({ if (!messageListRef.current) { return } - if (scrollTo === null) { + if (viewState.scrollTo === null) { return } + const { scrollTo, lastKnownScrollHeight } = viewState + log.debug( 'scrollTo: ' + scrollTo.type, 'scrollTop:', @@ -296,7 +300,7 @@ export default function MessageList({ onScroll(null) }, 0) }, 0) - }, [onScroll, scrollTo, lastKnownScrollHeight]) + }, [onScroll, viewState, viewState.scrollTo, viewState.lastKnownScrollHeight]) useLayoutEffect(() => { if (!refComposer.current) { @@ -324,7 +328,7 @@ export default function MessageList({ ) => void oldestFetchedMessageIndex: number - messageIds: number[] + messageListItems: T.MessageListItem[] messagePages: ChatStoreState['messagePages'] messageListRef: React.MutableRefObject chatStore: ChatStoreStateWithChatSet @@ -359,7 +363,7 @@ export const MessageListInner = React.memo( }) => { const { onScroll, - messageIds, + messageListItems, messagePages, messageListRef, chatStore, @@ -399,7 +403,7 @@ export const MessageListInner = React.memo( return (
    - {messageIds.length === 0 && } + {messageListItems.length === 0 && } {messagePages.map(messagePage => { return ( { - const areEqual = - prevProps.messageIds === nextProps.messageIds && + const areEqual: boolean = + prevProps.messageListItems === nextProps.messageListItems && prevProps.messagePages === nextProps.messagePages && prevProps.oldestFetchedMessageIndex === nextProps.oldestFetchedMessageIndex && @@ -466,33 +470,35 @@ const MessagePageComponent = React.memo( }) { const messageElements = [] const messagesOnPage = messagePage.messages.toArray() - for (let i = 0; i < messagesOnPage.length; i++) { const [messageId, message] = messagesOnPage[i] - if (message === null || message == undefined) continue - if (!message) { - log.debug(`Missing message with id ${messageId}`) - continue - } - if (messagePage.dayMarker.indexOf(messageId) !== -1) { + if (typeof messageId === 'string') { + // daymarker messageElements.push( + ) + } else { + // message + messageElements.push( + ) } - messageElements.push( - - ) } return ( diff --git a/src/renderer/components/message/MessageListAndComposer.tsx b/src/renderer/components/message/MessageListAndComposer.tsx index 041813f68d..fb65d1351d 100644 --- a/src/renderer/components/message/MessageListAndComposer.tsx +++ b/src/renderer/components/message/MessageListAndComposer.tsx @@ -1,5 +1,4 @@ import React, { useRef, useContext, useEffect } from 'react' -import { DeltaBackend } from '../../delta-remote' import Composer, { useDraft } from '../composer/Composer' import { getLogger } from '../../../shared/logger' import MessageList from './MessageList' @@ -31,11 +30,11 @@ export function getBackgroundImageStyle( // migrating in case of absolute filepaths const filePath = parse(bgImg.slice(5, bgImg.length - 2)).base bgImg = `img: ${filePath}` - DeltaBackend.call('settings.setDesktopSetting', 'chatViewBgImg', bgImg) + runtime.setDesktopSetting('chatViewBgImg', bgImg) } else if (bgImg.startsWith('#')) { // migrating to new prefixes bgImg = `color: ${bgImg}` - DeltaBackend.call('settings.setDesktopSetting', 'chatViewBgImg', bgImg) + runtime.setDesktopSetting('chatViewBgImg', bgImg) } if (bgImg.startsWith('img: ')) { const filePath = bgImg.slice(5) diff --git a/src/renderer/components/message/MessageMarkdown.tsx b/src/renderer/components/message/MessageMarkdown.tsx index 8713b1a6a6..3f02e0552d 100644 --- a/src/renderer/components/message/MessageMarkdown.tsx +++ b/src/renderer/components/message/MessageMarkdown.tsx @@ -5,7 +5,6 @@ import { ParsedElement, } from '@deltachat/message_parser_wasm/message_parser_wasm' import { getLogger } from '../../../shared/logger' -import { DeltaBackend } from '../../delta-remote' import { ActionEmitter, KeybindAction } from '../../keybindings' import { MessagesDisplayContext } from '../../contexts' import { selectChat, setChatView } from '../helpers/ChatMethods' @@ -100,11 +99,11 @@ export function message2React(message: string): JSX.Element { function EmailLink({ email }: { email: string }): JSX.Element { const openChatWithEmail = async () => { const accountId = selectedAccountId() - let contactId = await DeltaBackend.call( - 'contacts.lookupContactIdByAddr', + let contactId = await BackendRemote.rpc.lookupContactIdByAddr( + accountId, email ) - if (contactId == 0) { + if (contactId === null) { contactId = await BackendRemote.rpc.contactsCreateContact( accountId, email, @@ -136,7 +135,7 @@ function TagLink({ tag }: { tag: string }) { `Clicked on a hastag, this should open search for the text "${tag}"` ) if (window.__chatlistSetSearch) { - window.__chatlistSetSearch(tag) + window.__chatlistSetSearch(tag, null) ActionEmitter.emitAction(KeybindAction.ChatList_FocusSearchInput) // TODO: If you wonder why the focus doesn't work - its because of jikstra's composer focus hacks // Which transfer the focus back to the composer instantly diff --git a/src/renderer/components/message/messageFunctions.ts b/src/renderer/components/message/messageFunctions.ts index 8df45b2304..8835533058 100644 --- a/src/renderer/components/message/messageFunctions.ts +++ b/src/renderer/components/message/messageFunctions.ts @@ -1,6 +1,5 @@ import { getLogger } from '../../../shared/logger' const log = getLogger('render/msgFunctions') -import { DeltaBackend } from '../../delta-remote' import { runtime } from '../../runtime' import { deleteMessage, selectChat } from '../helpers/ChatMethods' import { BackendRemote, Type } from '../../backend-com' @@ -80,15 +79,19 @@ export async function privateReply(msg: Type.Message) { } export async function openMessageHTML(messageId: number) { - const filepath = await DeltaBackend.call( - 'messageList.saveMessageHTML2Disk', + const content = await BackendRemote.rpc.getMessageHtml( + selectedAccountId(), messageId ) - runtime.openPath(filepath) + if (!content) { + log.error('openMessageHTML, message has no html content', { messageId }) + return + } + runtime.openMessageHTML(content) } export async function downloadFullMessage(messageId: number) { - await DeltaBackend.call('messageList.downloadFullMessage', messageId) + await BackendRemote.rpc.downloadFullMessage(selectedAccountId(), messageId) } export async function openWebxdc(messageId: number) { diff --git a/src/renderer/components/screens/AccountListScreen.tsx b/src/renderer/components/screens/AccountListScreen.tsx index d5481776dd..c67d7c6782 100644 --- a/src/renderer/components/screens/AccountListScreen.tsx +++ b/src/renderer/components/screens/AccountListScreen.tsx @@ -4,7 +4,6 @@ import { getLogger } from '../../../shared/logger' import debounce from 'debounce' import React, { useContext, useEffect, useMemo, useState } from 'react' import { useTranslationFunction, ScreenContext } from '../../contexts' -import { ipcBackend } from '../../ipc' import ScreenController from '../../ScreenController' import { Avatar } from '../Avatar' import { PseudoContact } from '../contact/Contact' @@ -15,7 +14,7 @@ import { } from '../dialogs/DeltaDialog' import filesizeConverter from 'filesize' import { BackendRemote, EffectfulBackendActions, Type } from '../../backend-com' -import { DeltaBackend } from '../../delta-remote' +import { runtime } from '../../runtime' const log = getLogger('renderer/components/AccountsScreen') @@ -34,9 +33,7 @@ export default function AccountListScreen({ useEffect(() => { ;(async () => { - const desktopSettings = await DeltaBackend.call( - 'settings.getDesktopSettings' - ) + const desktopSettings = await runtime.getDesktopSettings() setSyncAllAccounts(desktopSettings.syncAllAccounts) })() }, []) @@ -93,11 +90,15 @@ export default function AccountListScreen({ label={tx('sync_all')} onChange={async () => { const new_state = !syncAllAccounts - await DeltaBackend.call( - 'settings.setDesktopSetting', + await runtime.setDesktopSetting( 'syncAllAccounts', new_state ) + if (new_state) { + BackendRemote.rpc.startIoForAllAccounts() + } else { + BackendRemote.rpc.stopIoForAllAccounts() + } setSyncAllAccounts(new_state) }} alignIndicator={Alignment.RIGHT} @@ -247,7 +248,8 @@ function AccountItem({ const [account_size, setSize] = useState('?') useEffect(() => { - DeltaBackend.call('login.getAccountSize', login.id) + BackendRemote.rpc + .getAccountFileSize(login.id) .catch(log.error) .then(bytes => { bytes && setSize(filesizeConverter(bytes)) @@ -258,7 +260,7 @@ function AccountItem({ const updateUnreadCount = useMemo( () => - debounce((_ev: any, account_id: number) => { + debounce((account_id: number) => { if (account_id === login.id) { BackendRemote.rpc .getFreshMsgs(login.id) @@ -270,14 +272,11 @@ function AccountItem({ ) useEffect(() => { - updateUnreadCount(null, login.id) - // TODO use onIncomingMsg event directly after we changed the events to be filtered for active account in the frontend - ipcBackend.on('DD_EVENT_INCOMING_MESSAGE_ACCOUNT', updateUnreadCount) + updateUnreadCount(login.id) + const emitter = BackendRemote.getContextEvents(login.id) + emitter.on('IncomingMsg', updateUnreadCount.bind(null, login.id)) return () => { - ipcBackend.removeListener( - 'DD_EVENT_INCOMING_MESSAGE_ACCOUNT', - updateUnreadCount - ) + emitter.off('IncomingMsg', updateUnreadCount.bind(null, login.id)) } }, [login.id, updateUnreadCount]) diff --git a/src/renderer/components/screens/MainScreen.tsx b/src/renderer/components/screens/MainScreen.tsx index 3db728dce4..a0cc432bcb 100644 --- a/src/renderer/components/screens/MainScreen.tsx +++ b/src/renderer/components/screens/MainScreen.tsx @@ -45,6 +45,7 @@ const log = getLogger('renderer/main-screen') export default function MainScreen() { const [queryStr, setQueryStr] = useState('') + const [queryChatId, setQueryChatId] = useState(null) const [sidebarState, setSidebarState] = useState('init') const [showArchivedChats, setShowArchivedChats] = useState(false) // Small hack/misuse of keyBindingAction to setShowArchivedChats from other components (especially @@ -64,9 +65,13 @@ export default function MainScreen() { selectChat(chatId) } - const searchChats = (queryStr: string) => setQueryStr(queryStr) - const handleSearchChange = (event: { target: { value: string } }) => - searchChats(event.target.value) + const searchChats = (queryStr: string, chatId: number | null = null) => { + setQueryStr(queryStr) + setQueryChatId(chatId) + } + const handleSearchChange = (event: { target: { value: string } }) => { + setQueryStr(event.target.value) + } const onTitleClick = () => { if (!selectedChat.chat) return @@ -116,6 +121,7 @@ export default function MainScreen() { } searchRef.current.value = '' searchChats('') + setQueryChatId(null) }) const onClickThreeDotMenu = useThreeDotMenu(selectedChat.chat) @@ -299,7 +305,11 @@ export default function MainScreen() { showArchivedChats={showArchivedChats} onChatClick={onChatClick} selectedChatId={selectedChat.chat ? selectedChat.chat.id : null} - onExitSearch={() => setQueryStr('')} + queryChatId={queryChatId} + onExitSearch={() => { + setQueryStr('') + setQueryChatId(null) + }} /> {MessageListView}
diff --git a/src/renderer/components/screens/WelcomeScreen.tsx b/src/renderer/components/screens/WelcomeScreen.tsx index ff9a2fa2e4..a91d84d7f6 100644 --- a/src/renderer/components/screens/WelcomeScreen.tsx +++ b/src/renderer/components/screens/WelcomeScreen.tsx @@ -1,10 +1,7 @@ import { Classes, Card, Elevation, Intent } from '@blueprintjs/core' -import { IpcRendererEvent } from 'electron' import React, { useEffect, useState, useContext } from 'react' import { getLogger } from '../../../shared/logger' import { ScreenContext, useTranslationFunction } from '../../contexts' -import { DeltaBackend } from '../../delta-remote' -import { ipcBackend } from '../../ipc' import { runtime } from '../../runtime' import DeltaDialog, { DeltaDialogBase, @@ -14,9 +11,10 @@ import DeltaDialog, { } from '../dialogs/DeltaDialog' import { DeltaProgressBar } from '../Login-Styles' import { DialogProps } from '../dialogs/DialogController' -import { Screens } from '../../ScreenController' +import { Screens, selectedAccountId } from '../../ScreenController' import { BackendRemote, EffectfulBackendActions } from '../../backend-com' import processOpenQrUrl from '../helpers/OpenQrUrl' +import { DcEventType } from '@deltachat/jsonrpc-client' const log = getLogger('renderer/components/AccountsScreen') @@ -28,22 +26,17 @@ function ImportBackupProgressDialog({ const [importProgress, setImportProgress] = useState(0.0) const [error, setError] = useState(null) - const onAll = (eventName: IpcRendererEvent, data1: string, data2: string) => { - log.debug('ALL core events: ', eventName, data1, data2) - } - const onImexProgress = (_evt: any, [progress, _data2]: [number, any]) => { + const onImexProgress = ({ progress }: DcEventType<'ImexProgress'>) => { setImportProgress(progress) } - const onError = (_data1: any, data2: string) => { - setError('DC_EVENT_ERROR: ' + data2) - } + const accountId = selectedAccountId() useEffect(() => { ;(async () => { - let account try { - account = await DeltaBackend.call('backup.import', backupFile) + log.debug(`Starting backup import of ${backupFile}`) + await BackendRemote.rpc.importBackup(accountId, backupFile, null) } catch (err) { if (err instanceof Error) { setError(err.message) @@ -51,19 +44,15 @@ function ImportBackupProgressDialog({ return } onClose() - window.__selectAccount(account.id) + window.__selectAccount(accountId) })() - ipcBackend.on('ALL', onAll) - ipcBackend.on('DC_EVENT_IMEX_PROGRESS', onImexProgress) - ipcBackend.on('DC_EVENT_ERROR', onError) - + const emitter = BackendRemote.getContextEvents(accountId) + emitter.on('ImexProgress', onImexProgress) return () => { - ipcBackend.removeListener('ALL', onAll) - ipcBackend.removeListener('DC_EVENT_IMEX_PROGRESS', onImexProgress) - ipcBackend.removeListener('DC_EVENT_ERROR', onError) + emitter.off('ImexProgress', onImexProgress) } - }, [backupFile, onClose]) + }, [backupFile, onClose, accountId]) const tx = useTranslationFunction() return ( diff --git a/src/renderer/delta-remote.ts b/src/renderer/delta-remote.ts index 781f3901c5..06c1d00f21 100644 --- a/src/renderer/delta-remote.ts +++ b/src/renderer/delta-remote.ts @@ -1,13 +1,4 @@ -import { C } from 'deltachat-node/node/dist/constants' import { _callDcMethodAsync } from './ipc' -import { - JsonLocations, - Theme, - MessageSearchResult, - DeltaChatAccount, - DesktopSettingsType, -} from '../shared/shared-types' -import { LocaleData } from '../shared/localize' export type sendMessageParams = { text?: string | null @@ -20,160 +11,8 @@ export type sendMessageParams = { } class DeltaRemote { - // root --------------------------------------------------------------- - call(fnName: 'setProfilePicture', newImage: string | null): Promise - call(fnName: 'joinSecurejoin', qrCode: string): Promise - // backup ------------------------------------------------------------- - call(fnName: 'backup.export', dir: string): Promise - call(fnName: 'backup.import', file: string): Promise - // chatList ----------------------------------------------------------- - call(fnName: 'chatList.onChatModified', chatId: number): Promise - // contacts ------------------------------------------------------------ - call( - fnName: 'contacts.changeNickname', - contactId: number, - name: string - ): Promise - call(fnName: 'contacts.lookupContactIdByAddr', email: string): Promise - call(fnName: 'contacts.deleteContact', contactId: number): Promise - // chat --------------------------------------------------------------- - call(fnName: 'chat.leaveGroup', chatId: number): Promise - call(fnName: 'chat.setName', chatId: number, name: string): Promise - call( - fnName: 'chat.modifyGroup', - chatId: number, - name: string, - image: string | undefined, - members: number[] | null - ): Promise - call( - fnName: 'chat.addContactToChat', - chatId: number, - contactId: number - ): Promise - call( - fnName: 'chat.setProfileImage', - chatId: number, - newImage: string - ): Promise - call(fnName: 'chat.createBroadcastList'): Promise - call( - fnName: 'chat.createGroupChat', - verified: boolean, - name: string - ): Promise - call( - fnName: 'chat.setVisibility', - chatId: number, - visibility: - | C.DC_CERTCK_AUTO - | C.DC_CERTCK_STRICT - | C.DC_CHAT_VISIBILITY_PINNED - ): Promise - call(fnName: 'chat.getChatContacts', chatId: number): Promise - call(fnName: 'chat.getChatEphemeralTimer', chatId: number): Promise - call( - fnName: 'chat.setChatEphemeralTimer', - chatId: number, - ephemeralTimer: number - ): Promise - call(fnName: 'chat.sendVideoChatInvitation', chatId: number): Promise - // locations ---------------------------------------------------------- - call( - fnName: 'locations.setLocation', - latitude: number, - longitude: number, - accuracy: number - ): Promise - call( - fnName: 'locations.getLocations', - chatId: number, - contactId: number, - timestampFrom: number, - timestampTo: number - ): Promise // login ---------------------------------------------------- call(fnName: 'login.selectAccount', accountId: number): Promise - call(fnName: 'login.getAccountSize', accountId: number): Promise - call(fnName: 'login.getLastLoggedInAccount'): Promise - - // NOTHING HERE that is called directly from the frontend, yet - // messageList -------------------------------------------------------- - call( - fnName: 'messageList.sendSticker', - chatId: number, - stickerPath: string - ): Promise - call(fnName: 'messageList.downloadFullMessage', msgId: number): Promise - call( - fnName: 'messageList.searchMessages', - query: string, - chatId?: number - ): Promise - call( - fnName: 'messageList.msgIds2SearchResultItems', - msgIds: number[] - ): Promise<{ [id: number]: MessageSearchResult }> - call( - fnName: 'messageList.saveMessageHTML2Disk', - messageId: number - ): Promise - // settings ----------------------------------------------------------- - call(fnName: 'settings.keysImport', directory: string): Promise - call(fnName: 'settings.keysExport', directory: string): Promise - call( - fnName: 'settings.serverFlags', - { - mail_security, - send_security, - }: { - mail_security?: string - send_security?: string - } - ): Promise - call( - fnName: 'settings.setDesktopSetting', - key: keyof DesktopSettingsType, - value: string | number | boolean - ): Promise - call(fnName: 'settings.getDesktopSettings'): Promise - call( - fnName: 'settings.saveBackgroundImage', - file: string, - isDefaultPicture: boolean - ): Promise - call( - fnName: 'settings.estimateAutodeleteCount', - fromServer: boolean, - seconds: number - ): Promise - // stickers ----------------------------------------------------------- - call( - fnName: 'stickers.getStickers' - ): Promise<{ - [key: string]: string[] - }> - // extras ------------------------------------------------------------- - call(fnName: 'extras.getLocaleData', locale: string): Promise - call(fnName: 'extras.setLocale', locale: string): Promise - call( - fnName: 'extras.getActiveTheme' - ): Promise<{ - theme: Theme - data: string - } | null> - call(fnName: 'extras.setThemeFilePath', address: string): void - call(fnName: 'extras.getAvailableThemes'): Promise - call(fnName: 'extras.setTheme', address: string): Promise - call(fnName: 'extras.writeClipboardToTempFile'): Promise - // webxdc: ------------------------------------------------------------ - call(fnName: 'webxdc.clearWebxdcDOMStorage'): Promise - call( - fnName: 'webxdc.getWebxdcDiskUsage' - ): Promise<{ - total_size: number - data_size: number - }> // catchall: ---------------------------------------------------------- call(fnName: string): Promise call(fnName: string, ...args: any[]): Promise { diff --git a/src/renderer/deviceMessages.ts b/src/renderer/deviceMessages.ts new file mode 100644 index 0000000000..45b6f2ae0c --- /dev/null +++ b/src/renderer/deviceMessages.ts @@ -0,0 +1,45 @@ +import { BUILD_TIMESTAMP, VERSION } from '../shared/build-info' +import { Timespans, DAYS_UNTIL_UPDATE_SUGGESTION } from '../shared/constants' +import { BackendRemote } from './backend-com' + +export function hintUpdateIfNessesary(accountId: number) { + if ( + Date.now() > + Timespans.ONE_DAY_IN_SECONDS * DAYS_UNTIL_UPDATE_SUGGESTION * 1000 + + BUILD_TIMESTAMP + ) { + BackendRemote.rpc.addDeviceMessage( + accountId, + `update-suggestion-${VERSION}`, + `This build is over ${DAYS_UNTIL_UPDATE_SUGGESTION} days old - There might be a new version available. -> https://get.delta.chat` + ) + } +} + +setInterval( + // If the dc is always on + () => { + if (window.__selectedAccountId) { + hintUpdateIfNessesary(window.__selectedAccountId) + } + }, + Timespans.ONE_DAY_IN_SECONDS * 1000 +) + +export function updateDeviceChats(accountId: number) { + BackendRemote.rpc.addDeviceMessage( + accountId, + 'changelog-version-1.33.0-version0', + `What's new in 1.33.0? + +- We made some speed improvements by moving to a new "backend" architecture +- We added some exiting new features: + - Clear chat + - Search in chat + - a recently seen indicator + +Thanks for testing DeltaChat, please report bugs on Github. + +Full changelog: https://github.com/deltachat/deltachat-desktop/blob/master/CHANGELOG.md` // no anchor link because this is a test version + ) +} diff --git a/src/renderer/global.d.ts b/src/renderer/global.d.ts index 5730097a4b..384edaebd1 100644 --- a/src/renderer/global.d.ts +++ b/src/renderer/global.d.ts @@ -30,7 +30,9 @@ declare global { __keybindingsDialogOpened: boolean __setQuoteInDraft: ((msgId: number) => void) | null __reloadDraft: (() => void) | null - __chatlistSetSearch: ((searchTerm: string) => void) | undefined + __chatlistSetSearch: + | ((searchTerm: string, chatId: number | null) => void) + | undefined __chatStore: any __refetchChatlist: undefined | (() => void) __welcome_qr: undefined | string diff --git a/src/renderer/ipc.ts b/src/renderer/ipc.ts index 2461e62ab5..1d646c513a 100644 --- a/src/renderer/ipc.ts +++ b/src/renderer/ipc.ts @@ -12,49 +12,6 @@ export const ipcBackend = ipcRenderer ipcBackend.setMaxListeners(20) -// Listen to DC/Backend events in a convenient way. Returns a callback to remove the -// event listener. You can bind the same event listener to multiple events by passing them -// as an array of strings. -export function onDCEvent( - event: string | string[], - cb: (data1: number, data2: string | number) => void -) { - const wrapperCb = (_: any, [data1, data2]: [number, string | number]) => { - cb(data1, data2) - } - if (Array.isArray(event)) { - event.forEach(e => ipcBackend.on(e, wrapperCb)) - return () => event.forEach(e => ipcBackend.removeListener(e, wrapperCb)) - } else { - ipcBackend.on(event, wrapperCb) - return () => ipcBackend.removeListener(event, wrapperCb) - } -} - -let backendLoggingStarted = false -export function startBackendLogging() { - if (backendLoggingStarted === true) - return log.error('Backend logging is already started!') - backendLoggingStarted = true - - ipcBackend.on('ALL', (_e, eName, data) => { - /* ignore-console-log */ - console.debug( - `%c📡 ${eName}`, - 'background:rgba(125,125,125,0.15);border-radius:2px;padding:2px 4px;', - data - ) - }) - - const log2 = getLogger('renderer') - window.addEventListener('error', event => { - log2.error('Unhandled Error:', event.error) - }) - window.addEventListener('unhandledrejection', event => { - log2.error('Unhandled Rejection:', event, event.reason) - }) -} - export function sendToBackend(event: string, ...args: any[]) { log.debug(`sendToBackend: ${event} ${args.join(' ')}`) ipcRenderer.send('ALL', event, ...args) diff --git a/src/renderer/onready.ts b/src/renderer/onready.ts new file mode 100644 index 0000000000..99dfb66e9b --- /dev/null +++ b/src/renderer/onready.ts @@ -0,0 +1,15 @@ +// it lets other scripts register callbacks that are called once everything is ready +// for stuff like listening for global events outside of react components like in the chat store + +let callbacks: (() => void)[] = [] + +/** make sure this is run when all mentioned variables are availible */ +export function onReady(cb: () => void) { + callbacks.push(cb) +} + +export function runPostponedFunctions() { + const todo = [...callbacks] + callbacks = [] + todo.forEach(cb => setTimeout(cb, 0)) +} diff --git a/src/renderer/runtime.ts b/src/renderer/runtime.ts index 8a3e60ce7d..4cce0ec546 100644 --- a/src/renderer/runtime.ts +++ b/src/renderer/runtime.ts @@ -5,6 +5,7 @@ import { DesktopSettingsType, RC_Config, RuntimeInfo, + Theme, } from '../shared/shared-types' import { setLogHandler } from '../shared/logger' import type { @@ -16,6 +17,7 @@ import type { } from 'electron' import { getLogger } from '../shared/logger' import processOpenQrUrl from './components/helpers/OpenQrUrl' +import { LocaleData } from '../shared/localize' const log = getLogger('renderer/runtime') @@ -43,7 +45,12 @@ const { * Offers an abstraction Layer to make it easier to make browser client in the future */ interface Runtime { + openMessageHTML(content: string): void getDesktopSettings(): Promise + setDesktopSetting( + key: keyof DesktopSettingsType, + value: string | number | boolean | undefined + ): Promise /** * initializes runtime stuff * - sets the LogHandler @@ -90,6 +97,10 @@ interface Runtime { // control app restartApp(): void + // translations + getLocaleData(locale?: string): Promise + setLocale(locale: string): Promise + // more system integration functions: setBadgeCounter(value: number): void showNotification(data: DcNotification): void @@ -98,15 +109,74 @@ interface Runtime { setNotificationCallback( cb: (data: { accountId: number; chatId: number; msgId: number }) => void ): void + writeClipboardToTempFile(): Promise + getWebxdcDiskUsage( + accountId: number + ): Promise<{ + total_size: number + data_size: number + }> + clearWebxdcDOMStorage(accountId: number): Promise + getAvailableThemes(): Promise + getActiveTheme(): Promise<{ + theme: Theme + data: string + } | null> + resolveThemeAddress(address: string): Promise + saveBackgroundImage(file: string, isDefaultPicture: boolean): Promise } class Browser implements Runtime { + openMessageHTML(_content: string): void { + throw new Error('Method not implemented.') + } notifyWebxdcStatusUpdate(_accountId: number, _instanceId: number): void { throw new Error('Method not implemented.') } notifyWebxdcInstanceDeleted(_accountId: number, _instanceId: number): void { throw new Error('Method not implemented.') } + saveBackgroundImage( + _file: string, + _isDefaultPicture: boolean + ): Promise { + throw new Error('Method not implemented.') + } + getLocaleData(_locale?: string | undefined): Promise { + throw new Error('Method not implemented.') + } + setLocale(_locale: string): Promise { + throw new Error('Method not implemented.') + } + setDesktopSetting( + _key: keyof DesktopSettingsType, + _value: string | number | boolean | undefined + ): Promise { + throw new Error('Method not implemented.') + } + getAvailableThemes(): Promise { + throw new Error('Method not implemented.') + } + async getActiveTheme(): Promise<{ theme: Theme; data: string } | null> { + return null + } + resolveThemeAddress(_address: string): Promise { + throw new Error('Method not implemented.') + } + clearWebxdcDOMStorage(_accountId: number): Promise { + throw new Error('Method not implemented.') + } + getWebxdcDiskUsage( + _accountId: number + ): Promise<{ + total_size: number + data_size: number + }> { + throw new Error('Method not implemented.') + } + async writeClipboardToTempFile(): Promise { + throw new Error('Method not implemented.') + } setNotificationCallback( _cb: (data: { accountId: number; chatId: number; msgId: number }) => void ): void { @@ -199,12 +269,50 @@ class Browser implements Runtime { } } class Electron implements Runtime { + openMessageHTML(content: string): void { + ipcBackend.invoke('openMessageHTML', content) + } notifyWebxdcStatusUpdate(accountId: number, instanceId: number): void { ipcBackend.invoke('webxdc:status-update', accountId, instanceId) } notifyWebxdcInstanceDeleted(accountId: number, instanceId: number): void { ipcBackend.invoke('webxdc:instance-deleted', accountId, instanceId) } + saveBackgroundImage( + file: string, + isDefaultPicture: boolean + ): Promise { + return ipcBackend.invoke('saveBackgroundImage', file, isDefaultPicture) + } + getLocaleData(locale?: string | undefined): Promise { + return ipcBackend.invoke('getLocaleData', locale) + } + setLocale(locale: string): Promise { + return ipcBackend.invoke('setLocale', locale) + } + getAvailableThemes(): Promise { + return ipcBackend.invoke('themes.getAvailableThemes') + } + getActiveTheme(): Promise<{ theme: Theme; data: string } | null> { + return ipcBackend.invoke('themes.getActiveTheme') + } + resolveThemeAddress(address: string): Promise { + return ipcBackend.invoke('themes.getAvailableThemes', address) + } + async clearWebxdcDOMStorage(accountId: number): Promise { + ipcBackend.invoke('webxdc.clearWebxdcDOMStorage', accountId) + } + getWebxdcDiskUsage( + accountId: number + ): Promise<{ + total_size: number + data_size: number + }> { + return ipcBackend.invoke('webxdc.getWebxdcDiskUsage', accountId) + } + async writeClipboardToTempFile(): Promise { + return ipcBackend.invoke('app.writeClipboardToTempFile') + } private notificationCallback: (data: { accountId: number chatId: number @@ -239,6 +347,12 @@ class Electron implements Runtime { getDesktopSettings(): Promise { return ipcBackend.invoke('get-desktop-settings') } + setDesktopSetting( + key: keyof DesktopSettingsType, + value: string | number | boolean | undefined + ): Promise { + return ipcBackend.invoke('set-desktop-setting', key, value) + } getWebxdcIconURL(msgId: number): string { return `webxdc-icon:${msgId}` } diff --git a/src/renderer/stockStrings.ts b/src/renderer/stockStrings.ts new file mode 100644 index 0000000000..707ff82938 --- /dev/null +++ b/src/renderer/stockStrings.ts @@ -0,0 +1,188 @@ +import { C } from 'deltachat-node/node/dist/constants' +import { getLogger } from '../shared/logger' +import { BackendRemote } from './backend-com' + +const log = getLogger('renderer/stockstrings') + +export async function updateCoreStrings() { + log.info('loading core translations') + const tx = window.static_translate + const strings: { [key: number]: string } = {} + // TODO: Check if we need the uncommented core translations + strings[C.DC_STR_NOMESSAGES] = tx('chat_no_messages') + strings[C.DC_STR_SELF] = tx('self') + strings[C.DC_STR_DRAFT] = tx('draft') + strings[C.DC_STR_VOICEMESSAGE] = tx('voice_message') + strings[C.DC_STR_IMAGE] = tx('image') + strings[C.DC_STR_GIF] = tx('gif') + strings[C.DC_STR_VIDEO] = tx('video') + strings[C.DC_STR_AUDIO] = tx('audio') + strings[C.DC_STR_FILE] = tx('file') + strings[C.DC_STR_ENCRYPTEDMSG] = tx('encrypted_message') + // strings[C.DC_STR_E2E_AVAILABLE] = tx('DC_STR_E2E_AVAILABLE') + // strings[C.DC_STR_ENCR_TRANSP] = tx('DC_STR_ENCR_TRANSP') + // strings[C.DC_STR_ENCR_NONE] = tx('DC_STR_ENCR_NONE') + strings[C.DC_STR_FINGERPRINTS] = tx('qrscan_fingerprint_label') + strings[C.DC_STR_CANTDECRYPT_MSG_BODY] = tx('systemmsg_cannot_decrypt') + strings[C.DC_STR_READRCPT] = tx('systemmsg_read_receipt_subject') + strings[C.DC_STR_READRCPT_MAILBODY] = tx('systemmsg_read_receipt_body') + strings[C.DC_STR_E2E_PREFERRED] = tx('autocrypt_prefer_e2ee') + strings[C.DC_STR_ARCHIVEDCHATS] = tx('chat_archived_chats_title') + strings[C.DC_STR_AC_SETUP_MSG_SUBJECT] = tx('autocrypt_asm_subject') + strings[C.DC_STR_AC_SETUP_MSG_BODY] = tx('autocrypt_asm_general_body') + strings[C.DC_STR_CANNOT_LOGIN] = tx('login_error_cannot_login') + strings[C.DC_STR_DEVICE_MESSAGES] = tx('device_talk') + strings[C.DC_STR_SAVED_MESSAGES] = tx('saved_messages') + strings[C.DC_STR_CONTACT_VERIFIED] = tx('contact_verified') + strings[C.DC_STR_CONTACT_NOT_VERIFIED] = tx('contact_not_verified') + strings[C.DC_STR_CONTACT_SETUP_CHANGED] = tx('contact_setup_changed') + strings[C.DC_STR_DEVICE_MESSAGES_HINT] = tx('device_talk_explain') + strings[C.DC_STR_WELCOME_MESSAGE] = tx('device_talk_welcome_message') + strings[C.DC_STR_UNKNOWN_SENDER_FOR_CHAT] = tx( + 'systemmsg_unknown_sender_for_chat' + ) + strings[C.DC_STR_SUBJECT_FOR_NEW_CONTACT] = tx( + 'systemmsg_subject_for_new_contact' + ) + strings[C.DC_STR_FAILED_SENDING_TO] = tx('systemmsg_failed_sending_to') + strings[C.DC_STR_VIDEOCHAT_INVITATION] = tx('videochat_invitation') + strings[C.DC_STR_VIDEOCHAT_INVITE_MSG_BODY] = tx('videochat_invitation_body') + strings[C.DC_STR_CONFIGURATION_FAILED] = tx('configuration_failed_with_error') + strings[C.DC_STR_REPLY_NOUN] = tx('reply_noun') + strings[C.DC_STR_FORWARDED] = tx('forwarded') + + //strings[C.DC_STR_MSGLOCATIONENABLED] = tx('') + //strings[C.DC_STR_MSGLOCATIONDISABLED] = tx('') + strings[C.DC_STR_LOCATION] = tx('location') + strings[C.DC_STR_STICKER] = tx('sticker') + strings[C.DC_STR_BAD_TIME_MSG_BODY] = tx('devicemsg_bad_time') + strings[C.DC_STR_UPDATE_REMINDER_MSG_BODY] = tx('devicemsg_update_reminder') + //strings[C.DC_STR_ERROR_NO_NETWORK] = tx('') + strings[C.DC_STR_SELF_DELETED_MSG_BODY] = tx('devicemsg_self_deleted') + //strings[C.DC_STR_SERVER_TURNED_OFF] = tx('') + strings[C.DC_STR_QUOTA_EXCEEDING_MSG_BODY] = tx('devicemsg_storage_exceeding') + strings[C.DC_STR_PARTIAL_DOWNLOAD_MSG_BODY] = tx('n_bytes_message') + strings[C.DC_STR_DOWNLOAD_AVAILABILITY] = tx('download_max_available_until') + //strings[C.DC_STR_SYNC_MSG_SUBJECT] = tx('') + //strings[C.DC_STR_SYNC_MSG_BODY] = tx('') + strings[C.DC_STR_INCOMING_MESSAGES] = tx('incoming_messages') + strings[C.DC_STR_OUTGOING_MESSAGES] = tx('outgoing_messages') + strings[C.DC_STR_STORAGE_ON_DOMAIN] = tx('storage_on_domain') + strings[C.DC_STR_ONE_MOMENT] = tx('one_moment') + strings[C.DC_STR_CONNECTED] = tx('connectivity_connected') + strings[C.DC_STR_CONNTECTING] = tx('connectivity_connecting') + strings[C.DC_STR_UPDATING] = tx('connectivity_updating') + strings[C.DC_STR_SENDING] = tx('sending') + strings[C.DC_STR_LAST_MSG_SENT_SUCCESSFULLY] = tx( + 'last_msg_sent_successfully' + ) + strings[C.DC_STR_ERROR] = tx('error_x') + strings[C.DC_STR_NOT_SUPPORTED_BY_PROVIDER] = tx('not_supported_by_provider') + strings[C.DC_STR_MESSAGES] = tx('messages') + strings[C.DC_STR_BROADCAST_LIST] = tx('broadcast_list') + strings[C.DC_STR_PART_OF_TOTAL_USED] = tx('part_of_total_used') + strings[C.DC_STR_SECURE_JOIN_STARTED] = tx('secure_join_started') + strings[C.DC_STR_SECURE_JOIN_REPLIES] = tx('secure_join_replies') + strings[C.DC_STR_SETUP_CONTACT_QR_DESC] = tx('qrshow_join_contact_hint') + strings[C.DC_STR_SECURE_JOIN_GROUP_QR_DESC] = tx('qrshow_join_group_hint') + strings[C.DC_STR_NOT_CONNECTED] = tx('connectivity_not_connected') + strings[C.DC_STR_AEAP_ADDR_CHANGED] = tx('aeap_addr_changed') + strings[C.DC_STR_AEAP_EXPLANATION_AND_LINK] = tx('aeap_explanation') + + strings[C.DC_STR_GROUP_NAME_CHANGED_BY_YOU] = tx('group_name_changed_by_you') + strings[C.DC_STR_GROUP_NAME_CHANGED_BY_OTHER] = tx( + 'group_name_changed_by_other' + ) + strings[C.DC_STR_GROUP_IMAGE_CHANGED_BY_YOU] = tx( + 'group_image_changed_by_you' + ) + strings[C.DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER] = tx( + 'group_image_changed_by_other' + ) + strings[C.DC_STR_ADD_MEMBER_BY_YOU] = tx('add_member_by_you') + strings[C.DC_STR_ADD_MEMBER_BY_OTHER] = tx('add_member_by_other') + strings[C.DC_STR_REMOVE_MEMBER_BY_YOU] = tx('remove_member_by_you') + strings[C.DC_STR_REMOVE_MEMBER_BY_OTHER] = tx('remove_member_by_other') + strings[C.DC_STR_GROUP_LEFT_BY_YOU] = tx('group_left_by_you') + strings[C.DC_STR_GROUP_LEFT_BY_OTHER] = tx('group_left_by_other') + strings[C.DC_STR_GROUP_IMAGE_DELETED_BY_YOU] = tx( + 'group_image_deleted_by_you' + ) + strings[C.DC_STR_GROUP_IMAGE_DELETED_BY_OTHER] = tx( + 'group_image_deleted_by_other' + ) + strings[C.DC_STR_LOCATION_ENABLED_BY_YOU] = tx('location_enabled_by_you') + strings[C.DC_STR_LOCATION_ENABLED_BY_OTHER] = tx('location_enabled_by_other') + strings[C.DC_STR_EPHEMERAL_TIMER_DISABLED_BY_YOU] = tx( + 'ephemeral_timer_disabled_by_you' + ) + strings[C.DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER] = tx( + 'ephemeral_timer_disabled_by_other' + ) + strings[C.DC_STR_EPHEMERAL_TIMER_SECONDS_BY_YOU] = tx( + 'ephemeral_timer_seconds_by_you' + ) + strings[C.DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER] = tx( + 'ephemeral_timer_seconds_by_other' + ) + strings[C.DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU] = tx( + 'ephemeral_timer_1_minute_by_you' + ) + strings[C.DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER] = tx( + 'ephemeral_timer_1_minute_by_other' + ) + strings[C.DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_YOU] = tx( + 'ephemeral_timer_1_hour_by_you' + ) + strings[C.DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER] = tx( + 'ephemeral_timer_1_hour_by_other' + ) + strings[C.DC_STR_EPHEMERAL_TIMER_1_DAY_BY_YOU] = tx( + 'ephemeral_timer_1_day_by_you' + ) + strings[C.DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER] = tx( + 'ephemeral_timer_1_day_by_other' + ) + strings[C.DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_YOU] = tx( + 'ephemeral_timer_1_week_by_you' + ) + strings[C.DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER] = tx( + 'ephemeral_timer_1_week_by_other' + ) + strings[C.DC_STR_EPHEMERAL_TIMER_MINUTES_BY_YOU] = tx( + 'ephemeral_timer_minutes_by_you' + ) + strings[C.DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER] = tx( + 'ephemeral_timer_minutes_by_other' + ) + strings[C.DC_STR_EPHEMERAL_TIMER_HOURS_BY_YOU] = tx( + 'ephemeral_timer_hours_by_you' + ) + strings[C.DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER] = tx( + 'ephemeral_timer_hours_by_other' + ) + strings[C.DC_STR_EPHEMERAL_TIMER_DAYS_BY_YOU] = tx( + 'ephemeral_timer_days_by_you' + ) + strings[C.DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER] = tx( + 'ephemeral_timer_days_by_other' + ) + strings[C.DC_STR_EPHEMERAL_TIMER_WEEKS_BY_YOU] = tx( + 'ephemeral_timer_weeks_by_you' + ) + strings[C.DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER] = tx( + 'ephemeral_timer_weeks_by_other' + ) + strings[C.DC_STR_PROTECTION_ENABLED_BY_YOU] = tx('protection_enabled_by_you') + strings[C.DC_STR_PROTECTION_ENABLED_BY_OTHER] = tx( + 'protection_enabled_by_other' + ) + strings[C.DC_STR_PROTECTION_DISABLED_BY_YOU] = tx( + 'protection_disabled_by_you' + ) + strings[C.DC_STR_PROTECTION_DISABLED_BY_OTHER] = tx( + 'protection_disabled_by_other' + ) + + await BackendRemote.rpc.setStockStrings(strings) +} diff --git a/src/renderer/stores/chat.ts b/src/renderer/stores/chat.ts index e784eadce2..744c33a09f 100644 --- a/src/renderer/stores/chat.ts +++ b/src/renderer/stores/chat.ts @@ -1,4 +1,3 @@ -import { ipcBackend } from '../ipc' import { Store, useStore } from './store' import { sendMessageParams } from '../delta-remote' import { ActionEmitter, KeybindAction } from '../keybindings' @@ -8,43 +7,20 @@ import { BackendRemote, Type } from '../backend-com' import { selectedAccountId } from '../ScreenController' import { debouncedUpdateBadgeCounter } from '../system-integration/badge-counter' import { clearNotificationsForChat } from '../system-integration/notifications' +import { T } from '@deltachat/jsonrpc-client' +import { ChatViewState } from './chat/chat_scroll' +import { ChatStoreScheduler } from './chat/chat_scheduler' +import { saveLastChatId } from './chat/chat_sideeffects' +import { onReady } from '../onready' export const PAGE_SIZE = 11 export interface MessagePage { pageKey: string - messages: OrderedMap - dayMarker: number[] -} - -type ScrollTo = - | ScrollToMessage - | ScrollToPosition - | ScrollToLastKnownPosition - | ScrollToBottom - | null - -interface ScrollToMessage { - type: 'scrollToMessage' - msgId: number - highlight: boolean -} - -interface ScrollToLastKnownPosition { - type: 'scrollToLastKnownPosition' - lastKnownScrollHeight: number - lastKnownScrollTop: number - appendedOn: 'top' | 'bottom' -} - -interface ScrollToPosition { - type: 'scrollToPosition' - scrollTop: number -} - -interface ScrollToBottom { - type: 'scrollToBottom' - ifClose: boolean + messages: OrderedMap< + number | string, + Type.Message | { id: string; ts: number } + > } export enum ChatView { @@ -56,87 +32,70 @@ export interface ChatStoreState { activeView: ChatView chat: Type.FullChat | null accountId?: number - messageIds: number[] + messageListItems: T.MessageListItem[] messagePages: MessagePage[] - newestFetchedMessageIndex: number + newestFetchedMessageListItemIndex: number oldestFetchedMessageIndex: number - scrollTo: ScrollTo - lastKnownScrollHeight: number - countFetchedMessages: number + viewState: ChatViewState jumpToMessageStack: number[] + countFetchedMessages: number } -const defaultState: ChatStoreState = { +const defaultState: () => ChatStoreState = () => ({ activeView: ChatView.MessageList, chat: null, accountId: undefined, - messageIds: [], + messageListItems: [], messagePages: [], - newestFetchedMessageIndex: -1, + newestFetchedMessageListItemIndex: -1, oldestFetchedMessageIndex: -1, - scrollTo: null, - lastKnownScrollHeight: -1, - countFetchedMessages: 0, + viewState: new ChatViewState(), jumpToMessageStack: [], -} - -function getLastKnownScrollPosition(): { - lastKnownScrollHeight: number - lastKnownScrollTop: number -} { - //@ts-ignore - const { scrollHeight, scrollTop } = document.querySelector('#message-list') - return { - lastKnownScrollHeight: scrollHeight, - lastKnownScrollTop: scrollTop, - } -} + countFetchedMessages: 0, +}) async function messagePageFromMessageIndexes( accountId: number, chatId: number, indexStart: number, indexEnd: number -) { - const _messages = await getMessagesFromIndex( +): Promise { + const rawMessages = await getMessagesFromIndex( accountId, chatId, indexStart, indexEnd ) - if (_messages.length === 0) { + if (rawMessages.length === 0) { throw new Error( 'messagePageFromMessageIndexes: _messages.length equals zero. This should not happen' ) } - if (_messages.length !== indexEnd - indexStart + 1) { + // log.debug('messagePageFromMessageIndexes', rawMessages, indexEnd, indexStart) + + if (rawMessages.length !== indexEnd - indexStart + 1) { throw new Error( "messagePageFromMessageIndexes: _messages.length doesn't equal indexEnd - indexStart + 1. This should not happen" ) } - const dayMarker: number[] = [] - const messages = OrderedMap().withMutations(messagePages => { - for (let i = 0; i < _messages.length; i++) { - const [messageId, message] = _messages[i] - if (messageId === C.DC_MSG_ID_DAYMARKER) { - if (!_messages[i + 1]) { - log.debug(`Had to skip DayMarker, I'm sorry`) - continue - } - dayMarker.push(_messages[i + 1][0]) - continue - } + const messages = OrderedMap< + number | string, + Type.Message | { id: string; ts: number } + >().withMutations(messagePages => { + for (let i = 0; i < rawMessages.length; i++) { + const [messageId, message] = rawMessages[i] messagePages.set(messageId, message) } - }) as OrderedMap + }) + + log.debug('messagePageFromMessageIndexes', messages) const messagePage = { pageKey: calculatePageKey(messages, indexStart, indexEnd), messages, - dayMarker, } return messagePage @@ -147,8 +106,8 @@ async function messagePagesFromMessageIndexes( chatId: number, indexStart: number, indexEnd: number -) { - const _messages = await getMessagesFromIndex( +): Promise { + const rawMessages = await getMessagesFromIndex( accountId, chatId, indexStart, @@ -156,73 +115,31 @@ async function messagePagesFromMessageIndexes( ) const messagePages = [] - - let currentIndex = 0 - while (currentIndex < _messages.length) { - const dayMarker: number[] = [] - const messages = OrderedMap().withMutations(messagePages => { - for (let i = currentIndex; i < currentIndex + PAGE_SIZE; i++) { - if (i >= _messages.length) break - const [messageId, message] = _messages[i] - if (messageId === C.DC_MSG_ID_DAYMARKER) { - if (!_messages[i + 1]) { - log.debug(`Had to skip DayMarker, I'm sorry`) - continue - } - dayMarker.push(_messages[i + 1][0]) - continue - } - + while (rawMessages.length > 0) { + const pageMessages = rawMessages.splice(0, PAGE_SIZE) + + const messages = OrderedMap< + number | string, + Type.Message | { id: string; ts: number } + >().withMutations(messagePages => { + for (let i = 0; i < pageMessages.length; i++) { + const [messageId, message] = pageMessages[i] messagePages.set(messageId, message) } - }) as OrderedMap - currentIndex = currentIndex + PAGE_SIZE + }) - const messagePage = { + messagePages.push({ pageKey: calculatePageKey(messages, indexStart, indexEnd), messages, - dayMarker, - } - - messagePages.push(messagePage) + }) } return messagePages } -function saveLastChatId(chatId: number) { - if (window.__selectedAccountId) { - BackendRemote.rpc.setConfig( - window.__selectedAccountId, - 'ui.lastchatid', - String(chatId) - ) - } -} - -interface ChatStoreLocks { - scroll: boolean - queue: boolean -} - class ChatStore extends Store { - __locks: ChatStoreLocks = { - scroll: false, - queue: false, - } - effectQueue: Function[] = [] - - lockUnlock(key: keyof ChatStoreLocks) { - this.__locks[key] = false - } + scheduler = new ChatStoreScheduler() - lockLock(key: keyof ChatStoreLocks) { - this.__locks[key] = true - } - - lockIsLocked(key: keyof ChatStoreLocks) { - return this.__locks[key] - } guardReducerTriesToAddDuplicatePageKey(pageKeyToAdd: string) { const isDuplicatePageKey = this.state.messagePages.findIndex( @@ -257,42 +174,35 @@ class ChatStore extends Store { }, selectedChat: (payload: Partial) => { this.setState(_ => { - this.lockUnlock('scroll') + this.scheduler.unlock('scroll') return { - ...defaultState, + ...defaultState(), ...payload, } }, 'selectedChat') }, refresh: ( - messageIds: number[], + messageListItems: T.MessageListItem[], messagePages: MessagePage[], newestFetchedMessageIndex: number, oldestFetchedMessageIndex: number ) => { this.setState(state => { - const { lastKnownScrollTop } = getLastKnownScrollPosition() - - const scrollTo: ScrollToPosition = { - type: 'scrollToPosition', - scrollTop: lastKnownScrollTop, - } - return { ...state, - messageIds, + messageListItems, messagePages, - scrollTo, - countFetchedMessages: messageIds.length, - newestFetchedMessageIndex, + viewState: state.viewState.refresh(), + countFetchedMessages: messageListItems.length, + newestFetchedMessageListItemIndex: newestFetchedMessageIndex, oldestFetchedMessageIndex, } }, 'refresh') }, unselectChat: () => { this.setState(_ => { - this.lockUnlock('scroll') - return { ...defaultState } + this.scheduler.unlock('scroll') + return { ...defaultState() } }, 'unselectChat') }, modifiedChat: (payload: { id: number } & Partial) => { @@ -312,23 +222,11 @@ class ChatStore extends Store { oldestFetchedMessageIndex: number }) => { this.setState(state => { - const { - lastKnownScrollHeight, - lastKnownScrollTop, - } = getLastKnownScrollPosition() - - const scrollTo: ScrollToLastKnownPosition = { - type: 'scrollToLastKnownPosition', - lastKnownScrollHeight, - lastKnownScrollTop, - appendedOn: 'top', - } - const modifiedState = { ...state, messagePages: [payload.messagePage, ...state.messagePages], oldestFetchedMessageIndex: payload.oldestFetchedMessageIndex, - scrollTo, + scrollTo: state.viewState.appendMessagePageTop(), countFetchedMessages: payload.countFetchedMessages, } if (this.guardReducerIfChatIdIsDifferent(payload)) return @@ -348,23 +246,11 @@ class ChatStore extends Store { newestFetchedMessageIndex: number }) => { this.setState(state => { - const { - lastKnownScrollHeight, - lastKnownScrollTop, - } = getLastKnownScrollPosition() - - const scrollTo: ScrollToLastKnownPosition = { - type: 'scrollToLastKnownPosition', - lastKnownScrollTop, - lastKnownScrollHeight, - appendedOn: 'bottom', - } - const modifiedState: ChatStoreState = { ...state, messagePages: [...state.messagePages, payload.messagePage], - newestFetchedMessageIndex: payload.newestFetchedMessageIndex, - scrollTo, + newestFetchedMessageListItemIndex: payload.newestFetchedMessageIndex, + viewState: state.viewState.appendMessagePageBottom(), countFetchedMessages: payload.countFetchedMessages, } if (this.guardReducerIfChatIdIsDifferent(payload)) return @@ -379,29 +265,17 @@ class ChatStore extends Store { }, fetchedIncomingMessages: (payload: { id: number - messageIds: ChatStoreState['messageIds'] + messageListItems: ChatStoreState['messageListItems'] newestFetchedMessageIndex: number messagePage: MessagePage }) => { this.setState(state => { - const { - lastKnownScrollHeight, - lastKnownScrollTop, - } = getLastKnownScrollPosition() - - const scrollTo: ScrollToBottom = { - type: 'scrollToBottom', - ifClose: true, - } - - const modifiedState = { + const modifiedState: ChatStoreState = { ...state, - messageIds: payload.messageIds, + messageListItems: payload.messageListItems, messagePages: [...state.messagePages, payload.messagePage], - newestFetchedMessageIndex: payload.newestFetchedMessageIndex, - lastKnownScrollHeight, - lastKnownScrollTop, - scrollTo, + // newestFetchedMessageIndex: payload.newestFetchedMessageIndex, + viewState: state.viewState.fetchedIncomingMessages(), } if ( @@ -425,33 +299,39 @@ class ChatStore extends Store { this.setState(state => { const modifiedState: ChatStoreState = { ...state, - scrollTo: null, - lastKnownScrollHeight: -1, + viewState: state.viewState.unlockScroll(), } if (this.guardReducerIfChatIdIsDifferent(payload)) return - setTimeout(() => this.lockUnlock('scroll'), 0) + setTimeout(() => this.scheduler.unlock('scroll'), 0) return modifiedState }, 'unlockScroll') }, uiDeleteMessage: (payload: { id: number; msgId: number }) => { this.setState(state => { const { msgId } = payload - const messageIndex = state.messageIds.indexOf(msgId) - let { oldestFetchedMessageIndex, newestFetchedMessageIndex } = state + const messageIndex = state.messageListItems.findIndex( + m => m.kind === 'message' && m.msg_id == msgId + ) + let { + oldestFetchedMessageIndex, + newestFetchedMessageListItemIndex: newestFetchedMessageIndex, + } = state if (messageIndex === oldestFetchedMessageIndex) { oldestFetchedMessageIndex += 1 } else if (messageIndex === newestFetchedMessageIndex) { newestFetchedMessageIndex -= 1 } - const messageIds = state.messageIds.filter(mId => mId !== msgId) + const messageListItems = state.messageListItems.filter( + m => m.kind !== 'message' || m.msg_id !== msgId + ) const modifiedState = { ...state, - messageIds, + messageListItems, messagePages: state.messagePages.map(messagePage => { if (messagePage.messages.has(msgId)) { return { ...messagePage, - messages: messagePage.messages.set(msgId, null), + messages: messagePage.messages.delete(msgId), } } return messagePage @@ -490,39 +370,6 @@ class ChatStore extends Store { return modifiedState }, 'messageChanged') }, - messageSent: (payload: { - id: number - messageId: number - message: Type.Message - }) => { - const { messageId, message } = payload - this.setState(state => { - const messageIds = [...state.messageIds, messageId] - const messagePages: MessagePage[] = [ - ...state.messagePages, - { - pageKey: `page-${messageId}-${messageId}`, - messages: OrderedMap().set(messageId, message) as OrderedMap< - number, - Type.Message | null - >, - dayMarker: [], - }, - ] - const scrollTo: ScrollToBottom = { - type: 'scrollToBottom', - ifClose: false, - } - const modifiedState = { - ...state, - messageIds, - messagePages, - scrollTo, - } - if (this.guardReducerIfChatIdIsDifferent(payload)) return - return modifiedState - }, 'messageSent') - }, setMessageState: (payload: { id: number messageId: number @@ -555,196 +402,43 @@ class ChatStore extends Store { return modifiedState }, 'setMessageState') }, - setMessageIds: (payload: { id: number; messageIds: number[] }) => { + setMessageListItems: (payload: { + id: number + messageListItems: ChatStoreState['messageListItems'] + }) => { this.setState(state => { - const { - lastKnownScrollHeight, - lastKnownScrollTop, - } = getLastKnownScrollPosition() - - const scrollTo: ScrollToLastKnownPosition = { - type: 'scrollToLastKnownPosition', - lastKnownScrollHeight, - lastKnownScrollTop, - appendedOn: 'top', - } - const modifiedState = { + const modifiedState: ChatStoreState = { ...state, - messageIds: payload.messageIds, - scrollTo, + messageListItems: payload.messageListItems, + viewState: state.viewState.setMessageListItems(), } if (this.guardReducerIfChatIdIsDifferent(payload)) return return modifiedState }, 'setMessageIds') }, - } - - // This effect will only get executed if the lock is unlocked. If it's still locked, this effect - // will get dropped/not executed. If you want the effect to get executed as soon as the lock with - // lockNameis unlocked, use lockedQueuedEffect. - lockedEffect( - lockName: keyof ChatStoreLocks, - effect: T, - effectName: string - ): T { - const fn: T = ((async (...args: any) => { - if (this.lockIsLocked(lockName) === true) { - log.debug(`lockedEffect: ${effectName}: We're locked, dropping effect`) - return false - } - - //log.debug(`lockedEffect: ${effectName}: locking`) - this.lockLock(lockName) - let returnValue - try { - returnValue = await effect(...args) - } catch (err) { - log.error(`lockedEffect: ${effectName}: error in called effect: ${err}`) - this.lockUnlock(lockName) - return - } - if (returnValue === false) { - /*log.debug( - `lockedEffect: ${effectName}: return value was false, unlocking` - )*/ - this.lockUnlock(lockName) - } else { - /*log.debug( - `lockedEffect: ${effectName}: return value was NOT false, keeping it locked` - )*/ - } - return returnValue - }) as unknown) as T - return fn - } - - tickRunQueuedEffect() { - setTimeout(async () => { - log.debug('effectQueue: running queued effects') - if (this.effectQueue.length === 0) { - //log.debug('effectQueue: no more queued effects, unlocking') - this.lockUnlock('queue') - log.debug('effectQueue: finished') - return - } - - const effect = this.effectQueue.pop() - if (!effect) { - throw new Error( - `Undefined effect in effect queue? This should not happen. Effect is: ${JSON.stringify( - effect - )}` - ) - } - try { - await effect() - } catch (err) { - log.error(`tickRunQueuedEffect: error in effect: ${err}`) - } - this.tickRunQueuedEffect() - }, 0) - } - - // This effect will get added to the end of the queue. The queue is getting executed one after the other. - queuedEffect(effect: T, effectName: string): T { - const fn: T = ((async (...args: any) => { - const lockQueue = () => { - //log.debug(`queuedEffect: ${effectName}: locking`) - this.lockLock('queue') - } - const unlockQueue = () => { - this.lockUnlock('queue') - //log.debug(`queuedEffect: ${effectName}: unlocked`) - } - - if (this.lockIsLocked('queue') === true) { - log.debug( - `queuedEffect: ${effectName}: We're locked, adding effect to queue` - ) - this.effectQueue.push(effect.bind(this, ...args)) - return false - } - //log.debug(`queuedEffect: ${effectName}: locking`) - lockQueue() - let returnValue - try { - returnValue = await effect(...args) - } catch (err) { - log.error(`Error in queuedEffect ${effectName}: ${err}`) - unlockQueue() - return - } - if (this.effectQueue.length !== 0) { - this.tickRunQueuedEffect() - } else { - unlockQueue() - } - - //log.debug(`queuedEffect: ${effectName}: done`) - return returnValue - }) as unknown) as T - return fn - } - - // This effect is once the lock with lockName is unlocked. It will get postponed until the lock is free. - lockedQueuedEffect( - lockName: keyof ChatStoreLocks, - effect: T, - effectName: string - ): T { - const fn: T = ((async (...args: any) => { - const lockQueue = () => { - //log.debug(`lockedQueuedEffect: ${effectName}: locking`) - this.lockLock('queue') - } - const unlockQueue = () => { - this.lockUnlock('queue') - log.debug(`lockedQueuedEffect: ${effectName}: unlocked`) - } - - if (this.lockIsLocked('queue') === true) { - log.debug( - `lockedQueuedEffect: ${effectName}: We're locked, adding effect to queue` - ) - this.effectQueue.push(effect.bind(this, ...args)) - return false - } - - if (this.lockIsLocked(lockName) === true) { - log.debug( - `lockedQueuedEffect: ${effectName}: Lock "${lockName}" is locked, postponing effect in queue` - ) - this.effectQueue.push(effect.bind(this, ...args)) - return false - } - - //log.debug(`lockedQueuedEffect: ${effectName}: locking`) - lockQueue() - let returnValue - try { - returnValue = await effect(...args) - } catch (err) { - log.error( - `Error in lockedQueuedEffect ${effectName}: ${(err as Error).stack}` - ) - unlockQueue() - return - } - if (this.effectQueue.length !== 0) { - this.tickRunQueuedEffect() - } else { - unlockQueue() - } - - log.debug(`lockedQueuedEffect: ${effectName}: done`) - return returnValue - }) as unknown) as T - return fn + setFreshMessageCounter: (payload: { + id: number + freshMessageCounter: number + }) => { + const { freshMessageCounter } = payload + this.setState(state => { + if (!state.chat) return + const modifiedState: ChatStoreState = { + ...state, + chat: { + ...state.chat, + freshMessageCounter, + }, + } + if (this.guardReducerIfChatIdIsDifferent(payload)) return + return modifiedState + }, 'setMessageIds') + }, } effect = { - selectChat: this.lockedQueuedEffect( + selectChat: this.scheduler.lockedQueuedEffect( 'scroll', async (chatId: number) => { const accountId = selectedAccountId() @@ -759,7 +453,7 @@ class ChatStore extends Store { ) return } - const messageIds = await BackendRemote.rpc.messageListGetMessageIds( + const messageListItems = await BackendRemote.rpc.getMessageListItems( accountId, chatId, C.DC_GCM_ADDDAYMARKER @@ -789,15 +483,15 @@ class ChatStore extends Store { let oldestFetchedMessageIndex = -1 let newestFetchedMessageIndex = -1 let messagePage: MessagePage | null = null - if (messageIds.length !== 0) { + if (messageListItems.length !== 0) { // mesageIds.length = 1767 // oldestFetchedMessageIndex = 1767 - 1 = 1766 - 10 = 1756 // newestFetchedMessageIndex = 1766 oldestFetchedMessageIndex = Math.max( - messageIds.length - 1 - PAGE_SIZE, + messageListItems.length - 1 - PAGE_SIZE, 0 ) - newestFetchedMessageIndex = messageIds.length - 1 + newestFetchedMessageIndex = messageListItems.length - 1 messagePage = await messagePageFromMessageIndexes( accountId, @@ -807,18 +501,14 @@ class ChatStore extends Store { ) } - const scrollTo: ScrollToBottom = { - type: 'scrollToBottom', - ifClose: false, - } this.reducer.selectedChat({ chat, accountId, messagePages: messagePage === null ? [] : [messagePage], - messageIds, + messageListItems, oldestFetchedMessageIndex, - newestFetchedMessageIndex, - scrollTo, + newestFetchedMessageListItemIndex: newestFetchedMessageIndex, + viewState: this.state.viewState.selectChat(), }) ActionEmitter.emitAction( chat.archived @@ -833,8 +523,8 @@ class ChatStore extends Store { this.reducer.setView(view) }, // TODO: Probably this should be lockedQueuedEffect too? - jumpToMessage: this.queuedEffect( - this.lockedEffect( + jumpToMessage: this.scheduler.queuedEffect( + this.scheduler.lockedEffect( 'scroll', async ( msgId: number | undefined, @@ -873,16 +563,10 @@ class ChatStore extends Store { ) chatId = message.chatId } else { - jumpToMessageStack = [] - jumpToMessageId = this.state.messageIds[ - this.state.messageIds.length - 1 - ] - message = await BackendRemote.rpc.messageGetMessage( - accountId, - jumpToMessageId - ) - chatId = message.chatId - return this.effect.selectChat(chatId) + if (this.state.chat) { + this.effect.selectChat(this.state.chat.id) + } + return } } else if (addMessageIdToStack === undefined) { // reset jumpToMessageStack @@ -939,26 +623,29 @@ class ChatStore extends Store { ) return } - const messageIds = await BackendRemote.rpc.messageListGetMessageIds( + const messageListItems = await BackendRemote.rpc.getMessageListItems( accountId, chatId, C.DC_GCM_ADDDAYMARKER ) - const jumpToMessageIndex = messageIds.indexOf(jumpToMessageId) + const jumpToMessageIndex = messageListItems.findIndex( + m => m.kind === 'message' && m.msg_id === jumpToMessageId + ) + // calculate page indexes, so that jumpToMessageId is in the middle of the page let oldestFetchedMessageIndex = -1 let newestFetchedMessageIndex = -1 let messagePage: MessagePage | null = null const half_page_size = Math.ceil(PAGE_SIZE / 2) - if (messageIds.length !== 0) { + if (messageListItems.length !== 0) { oldestFetchedMessageIndex = Math.max( jumpToMessageIndex - half_page_size, 0 ) newestFetchedMessageIndex = Math.min( jumpToMessageIndex + half_page_size, - messageIds.length - 1 + messageListItems.length - 1 ) const countMessagesOnNewerSide = @@ -975,7 +662,7 @@ class ChatStore extends Store { newestFetchedMessageIndex = Math.min( newestFetchedMessageIndex + (half_page_size - countMessagesOnOlderSide), - messageIds.length - 1 + messageListItems.length - 1 ) } @@ -997,14 +684,13 @@ class ChatStore extends Store { chat, accountId, messagePages: [messagePage], - messageIds, + messageListItems, oldestFetchedMessageIndex, - newestFetchedMessageIndex, - scrollTo: { - type: 'scrollToMessage', - msgId: jumpToMessageId, - highlight, - }, + newestFetchedMessageListItemIndex: newestFetchedMessageIndex, + viewState: this.state.viewState.jumpToMessage( + jumpToMessageId, + highlight + ), jumpToMessageStack, }) ActionEmitter.emitAction( @@ -1019,6 +705,25 @@ class ChatStore extends Store { ), 'jumpToMessage' ), + markseenMessages: async (chatId: number, msgIds: number[]) => { + if (!this.state.chat || !this.state.chat.id) return + if (this.state.chat.id !== chatId) return + if (!this.state.accountId) { + throw new Error('Account Id unset') + } + + await BackendRemote.rpc.markseenMsgs(this.state.accountId, msgIds) + const freshMessageCounter = await BackendRemote.rpc.getFreshMsgCnt( + this.state.accountId, + this.state.chat.id + ) + this.reducer.setFreshMessageCounter({ + id: this.state.chat.id, + freshMessageCounter, + }) + + debouncedUpdateBadgeCounter() + }, uiDeleteMessage: (msgId: number) => { if (!this.state.accountId) { throw new Error('Account Id unset') @@ -1028,8 +733,8 @@ class ChatStore extends Store { const id = this.state.chat.id this.reducer.uiDeleteMessage({ id, msgId }) }, - fetchMoreMessagesTop: this.queuedEffect( - this.lockedEffect( + fetchMoreMessagesTop: this.scheduler.queuedEffect( + this.scheduler.lockedEffect( 'scroll', async () => { log.debug(`fetchMoreMessagesTop`) @@ -1052,13 +757,13 @@ class ChatStore extends Store { ) return false } - const fetchedMessageIds = state.messageIds.slice( + const fetchedMessageListItems = state.messageListItems.slice( oldestFetchedMessageIndex, lastMessageIndexOnLastPage ) - if (fetchedMessageIds.length === 0) { + if (fetchedMessageListItems.length === 0) { log.debug( - 'fetchMoreMessagesTop: fetchedMessageIds.length is zero, returning' + 'fetchMoreMessagesTop: fetchedMessageListItems.length is zero, returning' ) return false } @@ -1074,7 +779,7 @@ class ChatStore extends Store { id, messagePage, oldestFetchedMessageIndex, - countFetchedMessages: fetchedMessageIds.length, + countFetchedMessages: fetchedMessageListItems.length, }) return true }, @@ -1082,8 +787,8 @@ class ChatStore extends Store { ), 'fetchMoreMessagesTop' ), - fetchMoreMessagesBottom: this.queuedEffect( - this.lockedEffect( + fetchMoreMessagesBottom: this.scheduler.queuedEffect( + this.scheduler.lockedEffect( 'scroll', async () => { if (!this.state.accountId) { @@ -1095,28 +800,31 @@ class ChatStore extends Store { } const id = state.chat.id - const newestFetchedMessageIndex = state.newestFetchedMessageIndex + 1 - const newNewestFetchedMessageIndex = Math.min( - newestFetchedMessageIndex + PAGE_SIZE, - state.messageIds.length - 1 + const newestFetchedMessageListItemIndex = + state.newestFetchedMessageListItemIndex + 1 + const newNewestFetchedMessageListItemIndex = Math.min( + newestFetchedMessageListItemIndex + PAGE_SIZE, + state.messageListItems.length - 1 ) - if (newestFetchedMessageIndex === state.messageIds.length) { + if ( + newestFetchedMessageListItemIndex === state.messageListItems.length + ) { //log.debug('fetchMoreMessagesBottom: no more messages, returning') return false } log.debug(`fetchMoreMessagesBottom`) - const fetchedMessageIds = state.messageIds.slice( - newestFetchedMessageIndex, - newNewestFetchedMessageIndex + 1 + const fetchedMessageListItems = state.messageListItems.slice( + newestFetchedMessageListItemIndex, + newNewestFetchedMessageListItemIndex + 1 ) - if (fetchedMessageIds.length === 0) { + if (fetchedMessageListItems.length === 0) { log.debug( - 'fetchMoreMessagesBottom: fetchedMessageIds.length is zero, returning', + 'fetchMoreMessagesBottom: fetchedMessageListItems.length is zero, returning', JSON.stringify({ - newestFetchedMessageIndex, - newNewestFetchedMessageIndex, - messageIds: state.messageIds, + newestFetchedMessageIndex: newestFetchedMessageListItemIndex, + newNewestFetchedMessageIndex: newNewestFetchedMessageListItemIndex, + messageIds: state.messageListItems, }) ) return false @@ -1125,15 +833,15 @@ class ChatStore extends Store { const messagePage: MessagePage = await messagePageFromMessageIndexes( this.state.accountId, id, - newestFetchedMessageIndex, - newNewestFetchedMessageIndex + newestFetchedMessageListItemIndex, + newNewestFetchedMessageListItemIndex ) this.reducer.appendMessagePageBottom({ id, messagePage, - newestFetchedMessageIndex: newNewestFetchedMessageIndex, - countFetchedMessages: fetchedMessageIds.length, + newestFetchedMessageIndex: newNewestFetchedMessageListItemIndex, + countFetchedMessages: fetchedMessageListItems.length, }) return true }, @@ -1141,8 +849,8 @@ class ChatStore extends Store { ), 'fetchMoreMessagesBottom' ), - refresh: this.queuedEffect( - this.lockedEffect( + refresh: this.scheduler.queuedEffect( + this.scheduler.lockedEffect( 'scroll', async (payload: { chatId: number }) => { const { chatId: eventChatId } = payload @@ -1164,15 +872,18 @@ class ChatStore extends Store { if (!this.state.accountId) { throw new Error('no account set') } - const messageIds = await BackendRemote.rpc.messageListGetMessageIds( + const messageListItems = await BackendRemote.rpc.getMessageListItems( this.state.accountId, chatId, C.DC_GCM_ADDDAYMARKER ) - let { newestFetchedMessageIndex, oldestFetchedMessageIndex } = state + let { + newestFetchedMessageListItemIndex: newestFetchedMessageIndex, + oldestFetchedMessageIndex, + } = state newestFetchedMessageIndex = Math.min( newestFetchedMessageIndex, - messageIds.length - 1 + messageListItems.length - 1 ) oldestFetchedMessageIndex = Math.max(oldestFetchedMessageIndex, 0) @@ -1184,7 +895,7 @@ class ChatStore extends Store { ) this.reducer.refresh( - messageIds, + messageListItems, messagePages, newestFetchedMessageIndex, oldestFetchedMessageIndex @@ -1207,7 +918,7 @@ class ChatStore extends Store { payload.muteDuration ) }, - sendMessage: this.queuedEffect( + sendMessage: this.scheduler.queuedEffect( async (payload: { chatId: number; message: sendMessageParams }) => { log.debug('sendMessage') if ( @@ -1234,7 +945,7 @@ class ChatStore extends Store { }, 'sendMessage' ), - onEventChatModified: this.queuedEffect(async (chatId: number) => { + onEventChatModified: this.scheduler.queuedEffect(async (chatId: number) => { if (this.state.chat?.id !== chatId) { return } @@ -1249,90 +960,63 @@ class ChatStore extends Store { ), }) }, 'onEventChatModified'), - onEventMessageFailed: this.queuedEffect( - async (chatId: number, msgId: number) => { - const state = this.state - if (state.chat?.id !== chatId) return - if (!state.messageIds.includes(msgId)) { - // Hacking around https://github.com/deltachat/deltachat-desktop/issues/1361#issuecomment-776291299 - - if (!this.state.accountId) { - throw new Error('no account set') - } - try { - const message = await BackendRemote.rpc.messageGetMessage( - this.state.accountId, - msgId - ) - this.reducer.messageSent({ - id: chatId, - messageId: msgId, - message, - }) - } catch (error) { - // ignore message not found, like the code that was previously here - return + onEventIncomingMessage: this.scheduler.queuedEffect( + async (chatId: number) => { + if (chatId !== this.state.chat?.id) { + log.debug( + `DC_EVENT_INCOMING_MSG chatId of event (${chatId}) doesn't match id of selected chat (${this.state.chat?.id}). Skipping.` + ) + return + } + if (!this.state.accountId) { + throw new Error('no account set') + } + const messageListItems = await BackendRemote.rpc.getMessageListItems( + this.state.accountId, + chatId, + C.DC_GCM_ADDDAYMARKER + ) + let indexStart = -1 + let indexEnd = -1 + for (let index = 0; index < messageListItems.length; index++) { + const msgListItem = messageListItems[index] + if (this.state.messageListItems.includes(msgListItem)) continue + if (indexStart === -1) { + indexStart = index } + indexEnd = index } - this.reducer.setMessageState({ + // Only add incoming messages if we could append them directly to messagePages without having a hole + if ( + this.state.newestFetchedMessageListItemIndex !== -1 && + indexStart !== this.state.newestFetchedMessageListItemIndex + 1 + ) { + log.debug( + `onEventIncomingMessage: new incoming messages cannot added to state without having a hole (indexStart: ${indexStart}, newestFetchedMessageListItemIndex ${this.state.newestFetchedMessageListItemIndex}), returning` + ) + this.reducer.setMessageListItems({ + id: chatId, + messageListItems, + }) + return + } + const messagePage = await messagePageFromMessageIndexes( + this.state.accountId, + chatId, + indexStart, + indexEnd + ) + + this.reducer.fetchedIncomingMessages({ id: chatId, - messageId: msgId, - messageState: C.DC_STATE_OUT_FAILED, + messageListItems, + messagePage, + newestFetchedMessageIndex: indexEnd, }) }, - 'onEventMessageFailed' + 'onEventIncomingMessage' ), - onEventIncomingMessage: this.queuedEffect(async (chatId: number) => { - if (chatId !== this.state.chat?.id) { - log.debug( - `DC_EVENT_INCOMING_MSG chatId of event (${chatId}) doesn't match id of selected chat (${this.state.chat?.id}). Skipping.` - ) - return - } - if (!this.state.accountId) { - throw new Error('no account set') - } - const messageIds = await BackendRemote.rpc.messageListGetMessageIds( - this.state.accountId, - chatId, - C.DC_GCM_ADDDAYMARKER - ) - let indexStart = -1 - let indexEnd = -1 - for (let index = 0; index < messageIds.length; index++) { - const msgId = messageIds[index] - if (this.state.messageIds.includes(msgId)) continue - if (indexStart === -1) { - indexStart = index - } - indexEnd = index - } - // Only add incoming messages if we could append them directly to messagePages without having a hole - if ( - this.state.newestFetchedMessageIndex !== -1 && - indexStart !== this.state.newestFetchedMessageIndex + 1 - ) { - log.debug( - `onEventIncomingMessage: new incoming messages cannot added to state without having a hole (indexStart: ${indexStart}, newestFetchedMessageIndex ${this.state.newestFetchedMessageIndex}), returning` - ) - this.reducer.setMessageIds({ id: chatId, messageIds }) - return - } - const messagePage = await messagePageFromMessageIndexes( - this.state.accountId, - chatId, - indexStart, - indexEnd - ) - - this.reducer.fetchedIncomingMessages({ - id: chatId, - messageIds, - messagePage, - newestFetchedMessageIndex: indexEnd, - }) - }, 'onEventIncomingMessage'), - onEventMessagesChanged: this.queuedEffect( + onEventMessagesChanged: this.scheduler.queuedEffect( async (eventChatId: number, messageId: number) => { log.debug('DC_EVENT_MSGS_CHANGED', eventChatId, messageId) const chatId = this.state.chat?.id @@ -1350,7 +1034,11 @@ class ChatStore extends Store { if (!this.state.accountId) { throw new Error('no account set') } - if (this.state.messageIds.indexOf(messageId) !== -1) { + if ( + this.state.messageListItems.findIndex( + m => m.kind === 'message' && m.msg_id === messageId + ) !== -1 + ) { log.debug( 'DC_EVENT_MSGS_CHANGED', 'changed message seems to be message we already know' @@ -1374,12 +1062,12 @@ class ChatStore extends Store { 'DC_EVENT_MSGS_CHANGED', 'changed message seems to be a new message, refetching messageIds' ) - const messageIds = await BackendRemote.rpc.messageListGetMessageIds( + const messageListItems = await BackendRemote.rpc.getMessageListItems( this.state.accountId, chatId, C.DC_GCM_ADDDAYMARKER ) - this.reducer.setMessageIds({ id: chatId, messageIds }) + this.reducer.setMessageListItems({ id: chatId, messageListItems }) } }, 'onEventMessagesChanged' @@ -1396,11 +1084,17 @@ class ChatStore extends Store { messages: messagePage.messages.toArray().map(([msgId, message]) => { return [ msgId, - message === null || message === undefined - ? null + typeof message.id !== 'string' + ? { + messageId: message.id, + messsage: (message as Type.Message).text, + } : { messageId: message.id, - messsage: message.text, + timestamp: (message as { + id: string + ts: number + }).ts, }, ] }), @@ -1410,7 +1104,7 @@ class ChatStore extends Store { } } -const chatStore = new ChatStore({ ...defaultState }, 'ChatStore') +const chatStore = new ChatStore({ ...defaultState() }, 'ChatStore') chatStore.dispatch = (..._args) => { throw new Error('Deprecated') @@ -1418,61 +1112,70 @@ chatStore.dispatch = (..._args) => { const log = chatStore.log -ipcBackend.on('DC_EVENT_CHAT_MODIFIED', async (_evt, [chatId]) => { - chatStore.effect.onEventChatModified(chatId) -}) +onReady(() => { + BackendRemote.on('ChatModified', (accountId, { chatId }) => { + if (accountId !== window.__selectedAccountId) { + return + } + chatStore.effect.onEventChatModified(chatId) + }) -ipcBackend.on('DC_EVENT_MSG_DELIVERED', (_evt, [id, msgId]) => { - chatStore.reducer.setMessageState({ - id, - messageId: msgId, - messageState: C.DC_STATE_OUT_DELIVERED, + BackendRemote.on('MsgDelivered', (accountId, { chatId: id, msgId }) => { + if (accountId !== window.__selectedAccountId) { + return + } + chatStore.reducer.setMessageState({ + id, + messageId: msgId, + messageState: C.DC_STATE_OUT_DELIVERED, + }) }) -}) -ipcBackend.on('DC_EVENT_MSG_FAILED', async (_evt, [chatId, msgId]) => { - chatStore.effect.onEventMessageFailed(chatId, msgId) -}) + BackendRemote.on('IncomingMsg', (accountId, { chatId }) => { + if (accountId !== window.__selectedAccountId) { + return + } + chatStore.effect.onEventIncomingMessage(chatId) + }) -ipcBackend.on('DC_EVENT_INCOMING_MSG', async (_, [chatId, _messageId]) => { - chatStore.effect.onEventIncomingMessage(chatId) -}) + BackendRemote.on('MsgRead', (accountId, { chatId: id, msgId }) => { + if (accountId !== window.__selectedAccountId) { + return + } + chatStore.reducer.setMessageState({ + id, + messageId: msgId, + messageState: C.DC_STATE_OUT_MDN_RCVD, + }) + }) -ipcBackend.on('DC_EVENT_MSG_READ', (_evt, [id, msgId]) => { - chatStore.reducer.setMessageState({ - id, - messageId: msgId, - messageState: C.DC_STATE_OUT_MDN_RCVD, + BackendRemote.on('MsgsChanged', (accountId, { chatId, msgId }) => { + if (accountId !== window.__selectedAccountId) { + return + } + chatStore.effect.onEventMessagesChanged(chatId, msgId) }) -}) -ipcBackend.on('DC_EVENT_MSGS_CHANGED', async (_, [eventChatId, messageId]) => { - chatStore.effect.onEventMessagesChanged(eventChatId, messageId) + BackendRemote.on('MsgFailed', (accountId, { chatId, msgId }) => { + if (accountId !== window.__selectedAccountId) { + return + } + chatStore.effect.onEventMessagesChanged(chatId, msgId) + }) }) export function calculatePageKey( - messages: OrderedMap, + messages: MessagePage['messages'], indexStart: number, indexEnd: number ): string { - const first = messages.find( - message => message !== null && message !== undefined - ) - const last = messages.findLast( - message => message !== null && message !== undefined - ) - let firstId = 0 - if (first) { - firstId = first.id - } - let lastId = 0 - if (last) { - lastId = last.id - } - if (firstId + lastId + indexStart + indexEnd === 0) { + const first = messages.first()?.id + const last = messages.last()?.id + if (!first && !last && indexStart === 0 && indexEnd === 0) { throw new Error('calculatePageKey: non unique page key of 0') + } else { + return `page-${first}-${last}-${indexStart}-${indexEnd}` } - return `page-${firstId}-${lastId}-${indexStart}-${indexEnd}` } export const useChatStore = () => useStore(chatStore)[0] @@ -1496,26 +1199,30 @@ async function getMessagesFromIndex( indexStart: number, indexEnd: number, flags = C.DC_GCM_ADDDAYMARKER -): Promise<[number, Type.Message][]> { - const allMessageIds = await BackendRemote.rpc.messageListGetMessageIds( - accountId, - chatId, - flags - ) +): Promise<[number | string, Type.Message | { id: string; ts: number }][]> { + const allMessageListItems = ( + await BackendRemote.rpc.getMessageListItems(accountId, chatId, flags) + ).slice(indexStart, indexEnd + 1) - const messageIds = allMessageIds - .slice(indexStart, indexEnd + 1) - .filter(msgid => msgid > C.DC_MSG_ID_LAST_SPECIAL) + const messageIds = allMessageListItems + .map(m => (m.kind === 'message' ? m.msg_id : C.DC_MSG_ID_LAST_SPECIAL)) + .filter(msgId => msgId !== C.DC_MSG_ID_LAST_SPECIAL) const messages = await BackendRemote.rpc.messageGetMessages( accountId, messageIds ) - const result: [number, Type.Message][] = messageIds.map(id => [ - id, - messages[id], - ]) + log.debug('getMessagesFromIndex', { + allMessageListItems, + messageIds, + messages, + }) - return result + return allMessageListItems.map(m => [ + m.kind === 'message' ? m.msg_id : `d${m.timestamp}`, + m.kind === 'message' + ? messages[m.msg_id] + : { id: `d${m.timestamp}`, ts: m.timestamp }, + ]) } diff --git a/src/renderer/stores/chat/chat_scheduler.ts b/src/renderer/stores/chat/chat_scheduler.ts new file mode 100644 index 0000000000..a439d586ba --- /dev/null +++ b/src/renderer/stores/chat/chat_scheduler.ts @@ -0,0 +1,193 @@ +import { getLogger } from '../../../shared/logger' + +const log = getLogger('renderer/stores/chat/scheduler') + +interface ChatStoreLocks { + scroll: boolean + queue: boolean +} + +export class ChatStoreScheduler { + private locks: ChatStoreLocks = { + scroll: false, + queue: false, + } + + effectQueue: Function[] = [] + + unlock(key: keyof ChatStoreLocks) { + this.locks[key] = false + } + + lock(key: keyof ChatStoreLocks) { + this.locks[key] = true + } + + isLocked(key: keyof ChatStoreLocks) { + return this.locks[key] + } + + /** This effect will only get executed if the lock is unlocked. If it's still locked, this effect + * will get dropped/not executed. If you want the effect to get executed as soon as the lock with + * lockNameis unlocked, use lockedQueuedEffect.*/ + lockedEffect( + lockName: keyof ChatStoreLocks, + effect: T, + effectName: string + ): T { + const fn: T = ((async (...args: any) => { + if (this.isLocked(lockName) === true) { + log.debug(`lockedEffect: ${effectName}: We're locked, dropping effect`) + return false + } + + //log.debug(`lockedEffect: ${effectName}: locking`) + this.lock(lockName) + let returnValue + try { + returnValue = await effect(...args) + } catch (err) { + log.error(`lockedEffect: ${effectName}: error in called effect: ${err}`) + this.unlock(lockName) + return + } + if (returnValue === false) { + /*log.debug( + `lockedEffect: ${effectName}: return value was false, unlocking` + )*/ + this.unlock(lockName) + } else { + /*log.debug( + `lockedEffect: ${effectName}: return value was NOT false, keeping it locked` + )*/ + } + return returnValue + }) as unknown) as T + return fn + } + + tickRunQueuedEffect() { + setTimeout(async () => { + log.debug('effectQueue: running queued effects') + if (this.effectQueue.length === 0) { + //log.debug('effectQueue: no more queued effects, unlocking') + this.unlock('queue') + log.debug('effectQueue: finished') + return + } + + const effect = this.effectQueue.pop() + if (!effect) { + throw new Error( + `Undefined effect in effect queue? This should not happen. Effect is: ${JSON.stringify( + effect + )}` + ) + } + try { + await effect() + } catch (err) { + log.error(`tickRunQueuedEffect: error in effect: ${err}`) + } + this.tickRunQueuedEffect() + }, 0) + } + + /** This effect will get added to the end of the queue. The queue is getting executed one after the other. */ + queuedEffect(effect: T, effectName: string): T { + const fn: T = ((async (...args: any) => { + const lockQueue = () => { + //log.debug(`queuedEffect: ${effectName}: locking`) + this.lock('queue') + } + const unlockQueue = () => { + this.unlock('queue') + //log.debug(`queuedEffect: ${effectName}: unlocked`) + } + + if (this.isLocked('queue') === true) { + log.debug( + `queuedEffect: ${effectName}: We're locked, adding effect to queue` + ) + this.effectQueue.push(effect.bind(this, ...args)) + return false + } + + //log.debug(`queuedEffect: ${effectName}: locking`) + lockQueue() + let returnValue + try { + returnValue = await effect(...args) + } catch (err) { + log.error(`Error in queuedEffect ${effectName}: ${err}`) + unlockQueue() + return + } + if (this.effectQueue.length !== 0) { + this.tickRunQueuedEffect() + } else { + unlockQueue() + } + + //log.debug(`queuedEffect: ${effectName}: done`) + return returnValue + }) as unknown) as T + return fn + } + + /** This effect is once the lock with lockName is unlocked. It will get postponed until the lock is free. */ + lockedQueuedEffect( + lockName: keyof ChatStoreLocks, + effect: T, + effectName: string + ): T { + const fn: T = ((async (...args: any) => { + const lockQueue = () => { + //log.debug(`lockedQueuedEffect: ${effectName}: locking`) + this.lock('queue') + } + const unlockQueue = () => { + this.unlock('queue') + log.debug(`lockedQueuedEffect: ${effectName}: unlocked`) + } + + if (this.isLocked('queue') === true) { + log.debug( + `lockedQueuedEffect: ${effectName}: We're locked, adding effect to queue` + ) + this.effectQueue.push(effect.bind(this, ...args)) + return false + } + + if (this.isLocked(lockName) === true) { + log.debug( + `lockedQueuedEffect: ${effectName}: Lock "${lockName}" is locked, postponing effect in queue` + ) + this.effectQueue.push(effect.bind(this, ...args)) + return false + } + + //log.debug(`lockedQueuedEffect: ${effectName}: locking`) + lockQueue() + let returnValue + try { + returnValue = await effect(...args) + } catch (err) { + log.error( + `Error in lockedQueuedEffect ${effectName}: ${(err as Error).stack}` + ) + unlockQueue() + return + } + if (this.effectQueue.length !== 0) { + this.tickRunQueuedEffect() + } else { + unlockQueue() + } + + log.debug(`lockedQueuedEffect: ${effectName}: done`) + return returnValue + }) as unknown) as T + return fn + } +} diff --git a/src/renderer/stores/chat/chat_scroll.ts b/src/renderer/stores/chat/chat_scroll.ts new file mode 100644 index 0000000000..24cef7e90f --- /dev/null +++ b/src/renderer/stores/chat/chat_scroll.ts @@ -0,0 +1,149 @@ +type ScrollTo = + | ScrollToMessage + | ScrollToPosition + | ScrollToLastKnownPosition + | ScrollToBottom + | null + +interface ScrollToMessage { + type: 'scrollToMessage' + msgId: number + highlight: boolean +} + +interface ScrollToLastKnownPosition { + type: 'scrollToLastKnownPosition' + lastKnownScrollHeight: number + lastKnownScrollTop: number + appendedOn: 'top' | 'bottom' +} + +interface ScrollToPosition { + type: 'scrollToPosition' + scrollTop: number +} + +interface ScrollToBottom { + type: 'scrollToBottom' + /** toggle proximity check, if on scroll only if close */ + ifClose: boolean +} + +export class ChatViewState { + scrollTo: ScrollTo = null + lastKnownScrollHeight = -1 + + refresh() { + // keep scroll position + const { lastKnownScrollTop } = getLastKnownScrollPosition() + this.scrollTo = { + type: 'scrollToPosition', + scrollTop: lastKnownScrollTop, + } + + return this + } + + appendMessagePageTop() { + const { + lastKnownScrollHeight, + lastKnownScrollTop, + } = getLastKnownScrollPosition() + + this.scrollTo = { + type: 'scrollToLastKnownPosition', + lastKnownScrollHeight, + lastKnownScrollTop, + appendedOn: 'top', + } + + return this + } + + appendMessagePageBottom() { + const { + lastKnownScrollHeight, + lastKnownScrollTop, + } = getLastKnownScrollPosition() + + this.scrollTo = { + type: 'scrollToLastKnownPosition', + lastKnownScrollTop, + lastKnownScrollHeight, + appendedOn: 'bottom', + } + + return this + } + + fetchedIncomingMessages() { + const { + lastKnownScrollHeight, + // lastKnownScrollTop, + } = getLastKnownScrollPosition() + + this.scrollTo = { + type: 'scrollToBottom', + ifClose: true, + } + + this.lastKnownScrollHeight = lastKnownScrollHeight + //this.lastKnownScrollTop = lastKnownScrollTop + + return this + } + + unlockScroll() { + this.scrollTo = null + this.lastKnownScrollHeight = -1 + + return this + } + + setMessageListItems() { + const { + lastKnownScrollHeight, + lastKnownScrollTop, + } = getLastKnownScrollPosition() + + this.scrollTo = { + type: 'scrollToLastKnownPosition', + lastKnownScrollHeight, + lastKnownScrollTop, + appendedOn: 'top', + } + + return this + } + + selectChat() { + this.scrollTo = { + type: 'scrollToBottom', + ifClose: false, + } + + return this + } + + jumpToMessage(jumpToMessageId: number, highlight: boolean) { + this.scrollTo = { + type: 'scrollToMessage', + msgId: jumpToMessageId, + highlight, + } + + return this + } +} + +function getLastKnownScrollPosition(): { + lastKnownScrollHeight: number + lastKnownScrollTop: number +} { + //@ts-ignore + const { scrollHeight, scrollTop } = document.querySelector('#message-list') + return { + lastKnownScrollHeight: scrollHeight, + lastKnownScrollTop: scrollTop, + } +} diff --git a/src/renderer/stores/chat/chat_sideeffects.ts b/src/renderer/stores/chat/chat_sideeffects.ts new file mode 100644 index 0000000000..b48c728b1b --- /dev/null +++ b/src/renderer/stores/chat/chat_sideeffects.ts @@ -0,0 +1,11 @@ +import { BackendRemote } from '../../backend-com' + +export function saveLastChatId(chatId: number) { + if (window.__selectedAccountId) { + BackendRemote.rpc.setConfig( + window.__selectedAccountId, + 'ui.lastchatid', + String(chatId) + ) + } +} diff --git a/src/renderer/stores/locations.ts b/src/renderer/stores/locations.ts index 8256809f13..09ea5998af 100644 --- a/src/renderer/stores/locations.ts +++ b/src/renderer/stores/locations.ts @@ -1,27 +1,26 @@ -import { JsonLocations } from '../../shared/shared-types' -import { Type } from '../backend-com' -import { DeltaBackend } from '../delta-remote' +import { DcEvent, T } from '@deltachat/jsonrpc-client' +import { BackendRemote, Type } from '../backend-com' +import { onReady } from '../onready' +import { selectedAccountId } from '../ScreenController' import { Store } from './store' -const { ipcRenderer } = window.electron_functions - export class state { selectedChat: Type.FullChat | null = null mapSettings = { timestampFrom: 0, timestampTo: 0, } - locations: JsonLocations = [] + locations: T.Location[] = [] } export const locationStore = new Store(new state(), 'location') const getLocations = async (chatId: number, mapSettings: todo) => { const { timestampFrom, timestampTo } = mapSettings - const locations: JsonLocations = await DeltaBackend.call( - 'locations.getLocations', + const locations: T.Location[] = await BackendRemote.rpc.getLocations( + selectedAccountId(), chatId, - 0, + null, timestampFrom, timestampTo ) @@ -30,33 +29,42 @@ const getLocations = async (chatId: number, mapSettings: todo) => { }, 'getLocations') } -const onLocationChange = (_evt: any, [chatId]: [number]) => { - const { selectedChat, mapSettings } = locationStore.getState() - if (selectedChat && chatId === selectedChat.id) { - getLocations(chatId, mapSettings) - } -} - -ipcRenderer.on('DC_EVENT_LOCATION_CHANGED', (_evt, contactId) => { - const { selectedChat, mapSettings } = locationStore.getState() - if (!selectedChat || !selectedChat.id) { - return - } - if (contactId === 0) { - // this means all locations were deleted - getLocations(selectedChat.id, mapSettings) - } - if (selectedChat && selectedChat.contactIds) { - const isMemberOfSelectedChat = selectedChat.contactIds.includes(contactId) - if (isMemberOfSelectedChat) { +onReady(() => { + BackendRemote.on('LocationChanged', (accountId, { contactId }) => { + if (accountId !== window.__selectedAccountId) { + return + } + const { selectedChat, mapSettings } = locationStore.getState() + if (!selectedChat || !selectedChat.id) { + return + } + if (contactId === null) { + // this means all locations were deleted getLocations(selectedChat.id, mapSettings) + } else if (selectedChat && selectedChat.contactIds) { + const isMemberOfSelectedChat = selectedChat.contactIds.includes(contactId) + if (isMemberOfSelectedChat) { + getLocations(selectedChat.id, mapSettings) + } + } + }) + + const onLocationChange = ( + accountId: number, + { chatId }: Extract + ) => { + if (accountId === window.__selectedAccountId) { + const { selectedChat, mapSettings } = locationStore.getState() + if (selectedChat && chatId === selectedChat.id) { + getLocations(chatId, mapSettings) + } } } -}) -// sometimes a MSGS_CHANGED is sent instead of locations changed -ipcRenderer.on('DC_EVENT_MSGS_CHANGED', onLocationChange) -ipcRenderer.on('DC_EVENT_INCOMING_MSG', onLocationChange) + // sometimes a MSGS_CHANGED is sent instead of locations changed + BackendRemote.on('MsgsChanged', onLocationChange) + BackendRemote.on('IncomingMsg', onLocationChange) +}) locationStore.attachReducer((action, state) => { if (action.type === 'DC_GET_LOCATIONS') { diff --git a/src/renderer/stores/settings.ts b/src/renderer/stores/settings.ts index df44ac747d..a66a3c2fe3 100644 --- a/src/renderer/stores/settings.ts +++ b/src/renderer/stores/settings.ts @@ -1,10 +1,8 @@ import { C } from 'deltachat-node/node/dist/constants' import { DesktopSettingsType, RC_Config } from '../../shared/shared-types' import { BackendRemote, Type } from '../backend-com' -import { DeltaBackend } from '../delta-remote' -import { ipcBackend } from '../ipc' +import { onReady } from '../onready' import { runtime } from '../runtime' -import { selectedAccountId } from '../ScreenController' import { Store, useStore } from './store' export interface SettingsStoreState { @@ -120,9 +118,7 @@ class SettingsStore extends Store { accountId, C.DC_CONTACT_ID_SELF ) - const desktopSettings = await DeltaBackend.call( - 'settings.getDesktopSettings' - ) + const desktopSettings = await runtime.getDesktopSettings() const rc = await runtime.getRC_Config() this.reducer.setState({ @@ -137,11 +133,11 @@ class SettingsStore extends Store { key: keyof DesktopSettingsType, value: string | number | boolean ) => { - if ( - (await DeltaBackend.call('settings.setDesktopSetting', key, value)) === - true - ) { + try { + await runtime.setDesktopSetting(key, value) this.reducer.setDesktopSetting(key, value) + } catch (error) { + this.log.error('failed to apply desktop setting:', error) } }, setCoreSetting: async ( @@ -165,13 +161,16 @@ class SettingsStore extends Store { } } -ipcBackend.on('DC_EVENT_SELFAVATAR_CHANGED', async (_evt, [_chatId]) => { - const accountId = selectedAccountId() - const selfContact = await BackendRemote.rpc.contactsGetContact( - accountId, - C.DC_CONTACT_ID_SELF - ) - SettingsStoreInstance.reducer.setSelfContact(selfContact) +onReady(() => { + BackendRemote.on('SelfavatarChanged', async accountId => { + if (accountId === window.__selectedAccountId) { + const selfContact = await BackendRemote.rpc.contactsGetContact( + accountId, + C.DC_CONTACT_ID_SELF + ) + SettingsStoreInstance.reducer.setSelfContact(selfContact) + } + }) }) const SettingsStoreInstance = new SettingsStore(null, 'SettingsStore') diff --git a/src/renderer/system-integration/badge-counter.ts b/src/renderer/system-integration/badge-counter.ts index 57f9bdceae..0e3739ab8b 100644 --- a/src/renderer/system-integration/badge-counter.ts +++ b/src/renderer/system-integration/badge-counter.ts @@ -1,6 +1,5 @@ import { debounce } from 'debounce' import { BackendRemote } from '../backend-com' -import { ipcBackend } from '../ipc' import { runtime } from '../runtime' async function updateBadgeCounter() { @@ -21,12 +20,11 @@ export const debouncedUpdateBadgeCounter = debounce( ) export function initBadgeCounter() { - ipcBackend.on('DC_EVENT_INCOMING_MSG', async (_, [_chatId, _messageId]) => { - debouncedUpdateBadgeCounter() + BackendRemote.on('IncomingMsg', accountId => { + if (accountId === window.__selectedAccountId) debouncedUpdateBadgeCounter() }) - - ipcBackend.on('DC_EVENT_CHAT_MODIFIED', async (_evt, [_chatId]) => { - debouncedUpdateBadgeCounter() + BackendRemote.on('ChatModified', accountId => { + if (accountId === window.__selectedAccountId) debouncedUpdateBadgeCounter() }) // on app startup: debouncedUpdateBadgeCounter() diff --git a/src/renderer/system-integration/notifications.ts b/src/renderer/system-integration/notifications.ts index ae548d3f15..631c67c33f 100644 --- a/src/renderer/system-integration/notifications.ts +++ b/src/renderer/system-integration/notifications.ts @@ -5,9 +5,7 @@ import { DcNotification } from '../../shared/shared-types' import { BackendRemote } from '../backend-com' import { isImage } from '../components/attachment/Attachment' import { jumpToMessage } from '../components/helpers/ChatMethods' -import { ipcBackend } from '../ipc' import { runtime } from '../runtime' -import { selectedAccountId } from '../ScreenController' import SettingsStoreInstance from '../stores/settings' const log = getLogger('renderer/notifications') @@ -16,9 +14,15 @@ function isMuted(accountId: number, chatId: number) { return BackendRemote.rpc.isChatMuted(accountId, chatId) } -async function incomingMessageHandler(chatId: number, messageId: number) { - // TODO check for account once we listen for the real event here - const accountId = selectedAccountId() +async function incomingMessageHandler( + accountId: number, + chatId: number, + messageId: number +) { + if (accountId !== window.__selectedAccountId) { + // notifications for different accounts are not supported yet + return + } log.debug('incomingMessageHandler: ', { chatId, messageId }) @@ -103,8 +107,8 @@ function getNotificationIcon( } export function initNotifications() { - ipcBackend.on('DC_EVENT_INCOMING_MSG', async (_, [chatId, messageId]) => { - incomingMessageHandler(chatId, messageId) + BackendRemote.on('IncomingMsg', (accountId, { chatId, msgId }) => { + incomingMessageHandler(accountId, chatId, msgId) }) runtime.setNotificationCallback(({ accountId, msgId, chatId }) => { if (window.__selectedAccountId !== accountId) { diff --git a/src/renderer/system-integration/webxdc.ts b/src/renderer/system-integration/webxdc.ts index c2ffb0ca75..13167118f8 100644 --- a/src/renderer/system-integration/webxdc.ts +++ b/src/renderer/system-integration/webxdc.ts @@ -3,16 +3,14 @@ // and heavyly uses the events import { BackendRemote } from '../backend-com' -import { ipcBackend } from '../ipc' import { runtime } from '../runtime' -import { selectedAccountId } from '../ScreenController' export function initWebxdc() { - ipcBackend.on('DC_EVENT_WEBXDC_STATUS_UPDATE', (_ev, [msgId]) => { - runtime.notifyWebxdcStatusUpdate(selectedAccountId(), msgId) + BackendRemote.on('WebxdcStatusUpdate', (accountId, { msgId }) => { + runtime.notifyWebxdcStatusUpdate(accountId, msgId) }) - ipcBackend.on('DC_EVENT_WEBXDC_INSTANCE_DELETED', (_ev, [msg_id]) => { - runtime.notifyWebxdcInstanceDeleted(selectedAccountId(), msg_id) + BackendRemote.on('WebxdcInstanceDeleted', (accountId, { msgId }) => { + runtime.notifyWebxdcInstanceDeleted(accountId, msgId) }) } diff --git a/src/shared/logger.ts b/src/shared/logger.ts index 0d5a37656c..0a861321be 100644 --- a/src/shared/logger.ts +++ b/src/shared/logger.ts @@ -47,6 +47,20 @@ export function printProcessLogLevelInfo() { )}`, emojiFontCss ) + /* ignore-console-log */ + console.info( + `# Tips and Tricks for working with the browser console: +## Use the search to filter the output like: +space seperate terms, exclude with -, if your term contains spaces you should exape it with " + +-👻 don't show events from background accounts (not selected accounts) +-📡 don't show any events +-renderer/jsonrpc don't show jsonrpc messages +renderer/jsonrpc show only jsonrpc messages + +Start deltachat with --devmode (or --log-debug and --log-to-console) argument to show full log output. + ` + ) } export type LogHandlerFunction = ( diff --git a/src/shared/shared-types.d.ts b/src/shared/shared-types.d.ts index 76b27a347a..62df4b6c98 100644 --- a/src/shared/shared-types.d.ts +++ b/src/shared/shared-types.d.ts @@ -81,18 +81,6 @@ export type JsonChat = ReturnType export type JsonContact = ReturnType -export type JsonLocations = { - accuracy: number - latitude: number - longitude: number - timestamp: number - contactId: number - msgId: number - chatId: number - isIndependent: boolean - marker: string -}[] // ReturnType - export type JsonMessage = ReturnType & { quote: MessageQuote | null } @@ -159,27 +147,6 @@ export type Theme = { is_prototype: boolean } -export type MessageSearchResult = { - id: number - authorProfileImage: string - author_name: string - author_color: string - chat_name: null | string - message: string - timestamp: number -} - -export type DeltaChatAccount = - | { id: number; type: 'unconfigured' } - | { - id: number - type: 'configured' - display_name: string | null - addr: string | null - profile_image: string | null - color: string - } - // Types that will stay: /** Additional info about the runtime the ui might need */