diff --git a/.eslintignore b/.eslintignore
index 9d378907f..c2862b719 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -462,6 +462,7 @@ packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExport
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/exportAllFolders.js
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
+packages/app-mobile/components/screens/LogScreen.js
packages/app-mobile/components/screens/Note.js
packages/app-mobile/components/screens/Notes.js
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
@@ -480,6 +481,7 @@ packages/app-mobile/services/voiceTyping/vosk.ios.js
packages/app-mobile/setupQuickActions.js
packages/app-mobile/tools/buildInjectedJs.js
packages/app-mobile/utils/ShareExtension.js
+packages/app-mobile/utils/ShareUtils.test.js
packages/app-mobile/utils/ShareUtils.js
packages/app-mobile/utils/TlsUtils.js
packages/app-mobile/utils/autodetectTheme.js
diff --git a/.gitignore b/.gitignore
index b46767877..f983f2af5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -448,6 +448,7 @@ packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExport
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/exportAllFolders.js
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
+packages/app-mobile/components/screens/LogScreen.js
packages/app-mobile/components/screens/Note.js
packages/app-mobile/components/screens/Notes.js
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
@@ -466,6 +467,7 @@ packages/app-mobile/services/voiceTyping/vosk.ios.js
packages/app-mobile/setupQuickActions.js
packages/app-mobile/tools/buildInjectedJs.js
packages/app-mobile/utils/ShareExtension.js
+packages/app-mobile/utils/ShareUtils.test.js
packages/app-mobile/utils/ShareUtils.js
packages/app-mobile/utils/TlsUtils.js
packages/app-mobile/utils/autodetectTheme.js
diff --git a/packages/app-mobile/components/screens/LogScreen.tsx b/packages/app-mobile/components/screens/LogScreen.tsx
new file mode 100644
index 000000000..b48e7ace4
--- /dev/null
+++ b/packages/app-mobile/components/screens/LogScreen.tsx
@@ -0,0 +1,195 @@
+import * as React from 'react';
+
+import { FlatList, View, Text, Button, StyleSheet, Platform, Alert } from 'react-native';
+import { connect } from 'react-redux';
+import { reg } from '@joplin/lib/registry.js';
+import { ScreenHeader } from '../ScreenHeader';
+import time from '@joplin/lib/time';
+const { themeStyle } = require('../global-style.js');
+import Logger from '@joplin/utils/Logger';
+const { BaseScreenComponent } = require('../base-screen.js');
+import { _ } from '@joplin/lib/locale';
+import { MenuOptionType } from '../ScreenHeader';
+import { AppState } from '../../utils/types';
+import Share from 'react-native-share';
+import { writeTextToCacheFile } from '../../utils/ShareUtils';
+import shim from '@joplin/lib/shim';
+
+const logger = Logger.create('LogScreen');
+
+class LogScreenComponent extends BaseScreenComponent {
+ private readonly menuOptions: MenuOptionType[];
+
+ public static navigationOptions(): any {
+ return { header: null };
+ }
+
+ public constructor() {
+ super();
+
+ this.state = {
+ logEntries: [],
+ showErrorsOnly: false,
+ };
+ this.styles_ = {};
+
+ this.menuOptions = [
+ {
+ title: _('Share'),
+ onPress: () => {
+ void this.onSharePress();
+ },
+ },
+ ];
+ }
+
+ private async onSharePress() {
+ const limit: number|null = null; // no limit
+ const levels = this.getLogLevels(this.state.showErrorsOnly);
+ const allEntries: any[] = await reg.logger().lastEntries(limit, { levels });
+ const logData = allEntries.map(entry => this.formatLogEntry(entry)).join('\n');
+
+ let fileToShare;
+ try {
+ // Using a .txt file extension causes a "No valid provider found from URL" error
+ // and blank share sheet on iOS for larger log files (around 200 KiB).
+ fileToShare = await writeTextToCacheFile(logData, 'mobile-log.log');
+
+ await Share.open({
+ type: 'text/plain',
+ filename: 'log.txt',
+ url: `file://${fileToShare}`,
+ failOnCancel: false,
+ });
+ } catch (e) {
+ logger.error('Unable to share log data:', e);
+
+ // Display a message to the user (e.g. in the case where the user is out of disk space).
+ Alert.alert(_('Error'), _('Unable to share log data. Reason: %s', e.toString()));
+ } finally {
+ if (fileToShare) {
+ await shim.fsDriver().remove(fileToShare);
+ }
+ }
+ }
+
+ public styles() {
+ const theme = themeStyle(this.props.themeId);
+
+ if (this.styles_[this.props.themeId]) return this.styles_[this.props.themeId];
+ this.styles_ = {};
+
+ const styles: any = {
+ row: {
+ flexDirection: 'row',
+ paddingLeft: 1,
+ paddingRight: 1,
+ paddingTop: 0,
+ paddingBottom: 0,
+ },
+ rowText: {
+ fontSize: 10,
+ color: theme.color,
+ },
+ };
+
+ if (Platform.OS !== 'ios') {
+ // Crashes on iOS with error "Unrecognized font family 'monospace'"
+ styles.rowText.fontFamily = 'monospace';
+ }
+
+ styles.rowTextError = { ...styles.rowText };
+ styles.rowTextError.color = theme.colorError;
+
+ styles.rowTextWarn = { ...styles.rowText };
+ styles.rowTextWarn.color = theme.colorWarn;
+
+ this.styles_[this.props.themeId] = StyleSheet.create(styles);
+ return this.styles_[this.props.themeId];
+ }
+
+ public UNSAFE_componentWillMount() {
+ void this.resfreshLogEntries();
+ }
+
+ private getLogLevels(showErrorsOnly: boolean) {
+ let levels = [Logger.LEVEL_DEBUG, Logger.LEVEL_INFO, Logger.LEVEL_WARN, Logger.LEVEL_ERROR];
+ if (showErrorsOnly) levels = [Logger.LEVEL_WARN, Logger.LEVEL_ERROR];
+
+ return levels;
+ }
+
+ private async resfreshLogEntries(showErrorsOnly: boolean = null) {
+ if (showErrorsOnly === null) showErrorsOnly = this.state.showErrorsOnly;
+
+ const levels = this.getLogLevels(showErrorsOnly);
+
+ this.setState({
+ logEntries: await reg.logger().lastEntries(1000, { levels: levels }),
+ showErrorsOnly: showErrorsOnly,
+ });
+ }
+
+ private toggleErrorsOnly() {
+ void this.resfreshLogEntries(!this.state.showErrorsOnly);
+ }
+
+ private formatLogEntry(item: any) {
+ return `${time.formatMsToLocal(item.timestamp, 'MM-DDTHH:mm:ss')}: ${item.message}`;
+ }
+
+ public render() {
+ const renderRow = ({ item }: any) => {
+ let textStyle = this.styles().rowText;
+ if (item.level === Logger.LEVEL_WARN) textStyle = this.styles().rowTextWarn;
+ if (item.level === Logger.LEVEL_ERROR) textStyle = this.styles().rowTextError;
+
+ return (
+
+ {this.formatLogEntry(item)}
+
+ );
+ };
+
+ // `enableEmptySections` is to fix this warning: https://github.com/FaridSafi/react-native-gifted-listview/issues/39
+
+ return (
+
+
+ { return `${item.id}`; }}
+ />
+
+
+
+
+
+
+
+ );
+ }
+}
+
+const LogScreen = connect((state: AppState) => {
+ return {
+ themeId: state.settings.theme,
+ };
+})(LogScreenComponent as any);
+
+export default LogScreen;
diff --git a/packages/app-mobile/components/screens/log.js b/packages/app-mobile/components/screens/log.js
deleted file mode 100644
index dd2893f80..000000000
--- a/packages/app-mobile/components/screens/log.js
+++ /dev/null
@@ -1,135 +0,0 @@
-const React = require('react');
-
-const { FlatList, View, Text, Button, StyleSheet, Platform } = require('react-native');
-const { connect } = require('react-redux');
-const { reg } = require('@joplin/lib/registry.js');
-const { ScreenHeader } = require('../ScreenHeader');
-const time = require('@joplin/lib/time').default;
-const { themeStyle } = require('../global-style.js');
-const Logger = require('@joplin/utils/Logger').default;
-const { BaseScreenComponent } = require('../base-screen.js');
-const { _ } = require('@joplin/lib/locale');
-
-class LogScreenComponent extends BaseScreenComponent {
- static navigationOptions() {
- return { header: null };
- }
-
- constructor() {
- super();
-
- this.state = {
- logEntries: [],
- showErrorsOnly: false,
- };
- this.styles_ = {};
- }
-
- styles() {
- const theme = themeStyle(this.props.themeId);
-
- if (this.styles_[this.props.themeId]) return this.styles_[this.props.themeId];
- this.styles_ = {};
-
- const styles = {
- row: {
- flexDirection: 'row',
- paddingLeft: 1,
- paddingRight: 1,
- paddingTop: 0,
- paddingBottom: 0,
- },
- rowText: {
- fontSize: 10,
- color: theme.color,
- },
- };
-
- if (Platform.OS !== 'ios') {
- // Crashes on iOS with error "Unrecognized font family 'monospace'"
- styles.rowText.fontFamily = 'monospace';
- }
-
- styles.rowTextError = { ...styles.rowText };
- styles.rowTextError.color = theme.colorError;
-
- styles.rowTextWarn = { ...styles.rowText };
- styles.rowTextWarn.color = theme.colorWarn;
-
- this.styles_[this.props.themeId] = StyleSheet.create(styles);
- return this.styles_[this.props.themeId];
- }
-
- UNSAFE_componentWillMount() {
- this.resfreshLogEntries();
- }
-
- async resfreshLogEntries(showErrorsOnly = null) {
- if (showErrorsOnly === null) showErrorsOnly = this.state.showErrorsOnly;
-
- let levels = [Logger.LEVEL_DEBUG, Logger.LEVEL_INFO, Logger.LEVEL_WARN, Logger.LEVEL_ERROR];
- if (showErrorsOnly) levels = [Logger.LEVEL_WARN, Logger.LEVEL_ERROR];
-
- this.setState({
- logEntries: await reg.logger().lastEntries(1000, { levels: levels }),
- showErrorsOnly: showErrorsOnly,
- });
- }
-
- toggleErrorsOnly() {
- this.resfreshLogEntries(!this.state.showErrorsOnly);
- }
-
- render() {
- const renderRow = ({ item }) => {
- let textStyle = this.styles().rowText;
- if (item.level === Logger.LEVEL_WARN) textStyle = this.styles().rowTextWarn;
- if (item.level === Logger.LEVEL_ERROR) textStyle = this.styles().rowTextError;
-
- return (
-
- {`${time.formatMsToLocal(item.timestamp, 'MM-DDTHH:mm:ss')}: ${item.message}`}
-
- );
- };
-
- // `enableEmptySections` is to fix this warning: https://github.com/FaridSafi/react-native-gifted-listview/issues/39
-
- return (
-
-
- { return `${item.id}`; }}
- />
-
-
-
-
-
-
-
- );
- }
-}
-
-const LogScreen = connect(state => {
- return {
- themeId: state.settings.theme,
- };
-})(LogScreenComponent);
-
-module.exports = { LogScreen };
diff --git a/packages/app-mobile/root.tsx b/packages/app-mobile/root.tsx
index 0c15669b3..8b159d102 100644
--- a/packages/app-mobile/root.tsx
+++ b/packages/app-mobile/root.tsx
@@ -60,7 +60,7 @@ import NotesScreen from './components/screens/Notes';
const { TagsScreen } = require('./components/screens/tags.js');
import ConfigScreen from './components/screens/ConfigScreen/ConfigScreen';
const { FolderScreen } = require('./components/screens/folder.js');
-const { LogScreen } = require('./components/screens/log.js');
+import LogScreen from './components/screens/LogScreen';
const { StatusScreen } = require('./components/screens/status.js');
const { SearchScreen } = require('./components/screens/search.js');
const { OneDriveLoginScreen } = require('./components/screens/onedrive-login.js');
diff --git a/packages/app-mobile/utils/ShareUtils.test.ts b/packages/app-mobile/utils/ShareUtils.test.ts
new file mode 100644
index 000000000..f641b7b0f
--- /dev/null
+++ b/packages/app-mobile/utils/ShareUtils.test.ts
@@ -0,0 +1,10 @@
+import { describe, test, expect } from '@jest/globals';
+import { pathExists } from 'fs-extra';
+import { writeTextToCacheFile } from './ShareUtils';
+
+describe('ShareUtils', () => {
+ test('writeTextFileToCache should write given text to a cache file', async () => {
+ const filePath1 = await writeTextToCacheFile('testing...', 'test1.txt');
+ expect(await pathExists(filePath1)).toBe(true);
+ });
+});
diff --git a/packages/app-mobile/utils/ShareUtils.ts b/packages/app-mobile/utils/ShareUtils.ts
index cdedf79a7..2b3978205 100644
--- a/packages/app-mobile/utils/ShareUtils.ts
+++ b/packages/app-mobile/utils/ShareUtils.ts
@@ -6,13 +6,18 @@ import { CachesDirectoryPath } from 'react-native-fs';
// when refactoring this name, make sure to refactor the `SharePackage.java` (in android) as well
const DIR_NAME = 'sharedFiles';
+const makeShareCacheDirectory = async () => {
+ const targetDir = `${CachesDirectoryPath}/${DIR_NAME}`;
+ await shim.fsDriver().mkdir(targetDir);
+
+ return targetDir;
+};
+
// Copy a file to be shared to cache, renaming it to its orignal name
export async function copyToCache(resource: ResourceEntity): Promise {
const filename = Resource.friendlySafeFilename(resource);
- const targetDir = `${CachesDirectoryPath}/${DIR_NAME}`;
- await shim.fsDriver().mkdir(targetDir);
-
+ const targetDir = await makeShareCacheDirectory();
const targetFile = `${targetDir}/${filename}`;
await shim.fsDriver().copy(Resource.fullPath(resource), targetFile);
@@ -20,6 +25,16 @@ export async function copyToCache(resource: ResourceEntity): Promise {
return targetFile;
}
+// fileName should be unique -- any file with fileName will be overwritten if it already exists.
+export const writeTextToCacheFile = async (text: string, fileName: string): Promise => {
+ const targetDir = await makeShareCacheDirectory();
+
+ const filePath = `${targetDir}/${fileName}`;
+ await shim.fsDriver().writeFile(filePath, text, 'utf8');
+
+ return filePath;
+};
+
// Clear previously shared files from cache
export async function clearSharedFilesCache(): Promise {
return shim.fsDriver().remove(`${CachesDirectoryPath}/sharedFiles`);
diff --git a/readme/debugging.md b/readme/debugging.md
index c1af5a1d3..e733e1c3a 100644
--- a/readme/debugging.md
+++ b/readme/debugging.md
@@ -32,7 +32,10 @@ There's two ways to start in safe mode:
## Mobile application
-- In the [Configuration screen](https://github.com/laurent22/joplin/blob/dev/readme/config_screen.md), press on the **Log button** and post a screenshot of any error/warning.
+- In the [Configuration screen](https://github.com/laurent22/joplin/blob/dev/readme/config_screen.md), press on the **Log button**, and from the options menu, press "share".
+- Attach the shared log (or just relevant portions) to the GitHub issue.
+
+If you recently (within two weeks) upgraded from 12.11.x to version 12.12.x, [be sure to check the log for and remove any sensitive data shared with Joplin](https://github.com/laurent22/joplin/issues/8211).
# Creating a low-level bug report on Android