diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 1209366318..520e849b98 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -91,6 +91,17 @@
+
+
+
diff --git a/android/app/src/main/java/im/status/ethereum/MainActivity.java b/android/app/src/main/java/im/status/ethereum/MainActivity.java
index 43e1796219..54b9ea8d70 100644
--- a/android/app/src/main/java/im/status/ethereum/MainActivity.java
+++ b/android/app/src/main/java/im/status/ethereum/MainActivity.java
@@ -30,7 +30,6 @@ import im.status.ethereum.module.StatusThreadPoolExecutor;
public class MainActivity extends ReactActivity
implements ActivityCompat.OnRequestPermissionsResultCallback{
- private static final int PERMISSION_REQUEST_CAMERA = 0;
@Nullable private PermissionListener mPermissionListener;
diff --git a/android/app/src/main/res/xml/file_provider_paths.xml b/android/app/src/main/res/xml/file_provider_paths.xml
new file mode 100644
index 0000000000..816c9d3f8c
--- /dev/null
+++ b/android/app/src/main/res/xml/file_provider_paths.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/desktop/CMakeModules/CompleteBundle.cmake b/desktop/CMakeModules/CompleteBundle.cmake
index a27268a872..39151c73c4 100644
--- a/desktop/CMakeModules/CompleteBundle.cmake
+++ b/desktop/CMakeModules/CompleteBundle.cmake
@@ -9,4 +9,4 @@ endif(APPLE)
if(SCRIPT AND EXISTS ${CMAKE_SOURCE_DIR}/CMakeModules/${SCRIPT}.cmake.in)
configure_file(${CMAKE_SOURCE_DIR}/CMakeModules/${SCRIPT}.cmake.in ${SCRIPT}.cmake @ONLY)
include(${CMAKE_CURRENT_BINARY_DIR}/${SCRIPT}.cmake)
-endif(SCRIPT)
+endif()
diff --git a/desktop/CMakeModules/QtConfiguration.cmake b/desktop/CMakeModules/QtConfiguration.cmake
index a81a07677b..47e756e20d 100644
--- a/desktop/CMakeModules/QtConfiguration.cmake
+++ b/desktop/CMakeModules/QtConfiguration.cmake
@@ -14,6 +14,10 @@ macro(import_qt_modules)
#message("${mod}_INCLUDE_DIRS: include_directories(${${mod}_INCLUDE_DIRS})")
include_directories(${${mod}_INCLUDE_DIRS})
+ if (${COMP} STREQUAL "Quick")
+ # We need to include the private headers for QZipWriter. If in the future we can't use that class anymore, we can always resort to the QuaZIP OSS library
+ include_directories(${${mod}_PRIVATE_INCLUDE_DIRS})
+ endif()
list(APPEND QT5_LIBRARIES ${${mod}_LIBRARIES})
list(APPEND QT5_CFLAGS ${${mod}_EXECUTABLE_COMPILE_FLAGS})
diff --git a/desktop/reportApp/CMakeLists.txt b/desktop/reportApp/CMakeLists.txt
index 5263b906d1..866cd0804f 100644
--- a/desktop/reportApp/CMakeLists.txt
+++ b/desktop/reportApp/CMakeLists.txt
@@ -14,7 +14,6 @@ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c11")
set(APP_NAME "reportApp")
set(MAIN_CPP_SOURCE reportpublisher.cpp
- reportpublisher.cpp
main.cpp)
add_executable(
@@ -23,7 +22,7 @@ add_executable(
main.qrc
)
-set(USED_QT_MODULES Core Qml Quick WebSockets Svg)
+set(USED_QT_MODULES Core Qml Quick WebSockets Widgets Svg)
qt5_use_modules(${APP_NAME} ${USED_QT_MODULES})
diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java
index c6dd2681f1..a93e5bc979 100644
--- a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java
+++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java
@@ -1,12 +1,17 @@
package im.status.ethereum.module;
import android.app.Activity;
+import android.app.AlertDialog;
import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.*;
-import android.view.WindowManager;
+import android.support.v4.content.FileProvider ;
import android.text.TextUtils;
+import android.view.WindowManager;
import android.util.Log;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
@@ -16,17 +21,25 @@ import com.facebook.react.bridge.*;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.github.status_im.status_go.Statusgo;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.FileInputStream;
-import java.io.OutputStream;
import java.io.FileOutputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.text.SimpleDateFormat;
+import java.util.Stack;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
import org.json.JSONObject;
import org.json.JSONException;
@@ -36,11 +49,12 @@ import javax.annotation.Nullable;
class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventListener, ConnectorHandler {
private static final String TAG = "StatusModule";
+ private static final String logsZipFileName = "Status-debug-logs.zip";
+ private static final String gethLogFileName = "geth.log";
+ private static final String statusLogFileName = "Status.log";
private final static int TESTNET_NETWORK_ID = 3;
- private HashMap callbacks = new HashMap<>();
-
private static StatusModule module;
private ServiceConnector status = null;
private ExecutorService executor = null;
@@ -114,10 +128,15 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
this.getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("gethEvent", params);
}
- private static String prepareLogsFile() {
- String gethLogFileName = "geth.log";
- File pubDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
- File logFile = new File(pubDirectory, gethLogFileName);
+ private File getLogsFile() {
+ final File pubDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
+ final File logFile = new File(pubDirectory, gethLogFileName);
+
+ return logFile;
+ }
+
+ private String prepareLogsFile(final Context context) {
+ final File logFile = getLogsFile();
try {
logFile.setReadable(true);
@@ -149,7 +168,8 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
// retrieve parameters from app config, that will be applied onto the Go-side config later on
final String absDataDirPath = pathCombine(absRootDirPath, jsonConfig.getString("DataDir"));
final Boolean logEnabled = jsonConfig.getBoolean("LogEnabled");
- final String gethLogFilePath = logEnabled ? prepareLogsFile() : null;
+ final Context context = this.getReactApplicationContext();
+ final String gethLogFilePath = logEnabled ? prepareLogsFile(context) : null;
jsonConfig.put("DataDir", absDataDirPath);
jsonConfig.put("KeyStoreDir", absKeystoreDirPath);
@@ -456,6 +476,156 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
StatusThreadPoolExecutor.getInstance().execute(r);
}
+ private Boolean zip(File[] _files, File zipFile, Stack errorList) {
+ final int BUFFER = 0x8000;
+
+ try {
+ BufferedInputStream origin = null;
+ FileOutputStream dest = new FileOutputStream(zipFile);
+ ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(dest));
+ byte data[] = new byte[BUFFER];
+
+ for (int i = 0; i < _files.length; i++) {
+ final File file = _files[i];
+ if (file == null || !file.exists()) {
+ continue;
+ }
+
+ Log.v("Compress", "Adding: " + file.getAbsolutePath());
+ try {
+ FileInputStream fi = new FileInputStream(file);
+ origin = new BufferedInputStream(fi, BUFFER);
+
+ ZipEntry entry = new ZipEntry(file.getName());
+ out.putNextEntry(entry);
+ int count;
+
+ while ((count = origin.read(data, 0, BUFFER)) != -1) {
+ out.write(data, 0, count);
+ }
+ origin.close();
+ } catch (IOException e) {
+ Log.e(TAG, e.getMessage());
+ errorList.push(e.getMessage());
+ }
+ }
+
+ out.close();
+
+ return true;
+ } catch (Exception e) {
+ Log.e(TAG, e.getMessage());
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ private void dumpAdbLogsTo(final FileOutputStream statusLogStream) throws IOException {
+ final String filter = "logcat -d -b main ReactNativeJS:D StatusModule:D StatusService:D StatusNativeLogs:D *:S";
+ final java.lang.Process p = Runtime.getRuntime().exec(filter);
+ final java.io.BufferedReader in = new java.io.BufferedReader(new java.io.InputStreamReader(p.getInputStream()));
+ final java.io.BufferedWriter out = new java.io.BufferedWriter(new java.io.OutputStreamWriter(statusLogStream));
+ String line;
+ while ((line = in.readLine()) != null) {
+ out.write(line);
+ out.newLine();
+ }
+ out.close();
+ in.close();
+ }
+
+ private void showErrorMessage(final String message) {
+ final Activity activity = getCurrentActivity();
+
+ new AlertDialog.Builder(activity)
+ .setTitle("Error")
+ .setMessage(message)
+ .setNegativeButton("Exit", new DialogInterface.OnClickListener() {
+ public void onClick(final DialogInterface dialog, final int id) {
+ dialog.dismiss();
+ }
+ }).show();
+ }
+
+ @ReactMethod
+ public void sendLogs(final String dbJson) {
+ Log.d(TAG, "sendLogs");
+ if (!checkAvailability()) {
+ return;
+ }
+
+ final Context context = this.getReactApplicationContext();
+ final File logsTempDir = new File(context.getCacheDir(), "logs"); // This path needs to be in sync with android/app/src/main/res/xml/file_provider_paths.xml
+ logsTempDir.mkdir();
+
+ final File dbFile = new File(logsTempDir, "db.json");
+ try {
+ OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(dbFile));
+ outputStreamWriter.write(dbJson);
+ outputStreamWriter.close();
+ }
+ catch (IOException e) {
+ Log.e(TAG, "File write failed: " + e.toString());
+ showErrorMessage(e.getLocalizedMessage());
+ }
+
+ final File zipFile = new File(logsTempDir, logsZipFileName);
+ final File statusLogFile = new File(logsTempDir, statusLogFileName);
+ final File gethLogFile = getLogsFile();
+
+ try {
+ if (zipFile.exists() || zipFile.createNewFile()) {
+ final long usableSpace = zipFile.getUsableSpace();
+ if (usableSpace < 20 * 1024 * 1024) {
+ final String message = String.format("Insufficient space available on device (%s) to write logs.\nPlease free up some space.", android.text.format.Formatter.formatShortFileSize(context, usableSpace));
+ Log.e(TAG, message);
+ showErrorMessage(message);
+ return;
+ }
+ }
+
+ dumpAdbLogsTo(new FileOutputStream(statusLogFile));
+
+ final Stack errorList = new Stack();
+ final Boolean zipped = zip(new File[] {dbFile, gethLogFile, statusLogFile}, zipFile, errorList);
+ if (zipped && zipFile.exists()) {
+ Log.d(TAG, "Sending " + zipFile.getAbsolutePath() + " file through share intent");
+
+ final String providerName = context.getPackageName() + ".provider";
+ final Activity activity = getCurrentActivity();
+ zipFile.setReadable(true, false);
+ final Uri dbJsonURI = FileProvider.getUriForFile(activity, providerName, zipFile);
+
+ Intent intentShareFile = new Intent(Intent.ACTION_SEND);
+
+ intentShareFile.setType("application/json");
+ intentShareFile.putExtra(Intent.EXTRA_STREAM, dbJsonURI);
+
+ SimpleDateFormat dateFormatGmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ dateFormatGmt.setTimeZone(java.util.TimeZone.getTimeZone("GMT"));
+ intentShareFile.putExtra(Intent.EXTRA_SUBJECT, "Status.im logs");
+ intentShareFile.putExtra(Intent.EXTRA_TEXT,
+ String.format("Logs from %s GMT\n\nThese logs have been generated automatically by the user's request for debugging purposes.\n\n%s",
+ dateFormatGmt.format(new java.util.Date()),
+ errorList));
+
+ activity.startActivity(Intent.createChooser(intentShareFile, "Share Debug Logs"));
+ } else {
+ Log.d(TAG, "File " + zipFile.getAbsolutePath() + " does not exist");
+ }
+ } catch (Exception e) {
+ Log.e(TAG, e.getMessage());
+ showErrorMessage(e.getLocalizedMessage());
+ e.printStackTrace();
+ return;
+ }
+ finally {
+ dbFile.delete();
+ statusLogFile.delete();
+ zipFile.deleteOnExit();
+ }
+ }
+
@ReactMethod
public void addPeer(final String enode, final Callback callback) {
Log.d(TAG, "addPeer");
diff --git a/modules/react-native-status/desktop/rctstatus.cpp b/modules/react-native-status/desktop/rctstatus.cpp
index 95ba0a69d3..78b11ee8f5 100644
--- a/modules/react-native-status/desktop/rctstatus.cpp
+++ b/modules/react-native-status/desktop/rctstatus.cpp
@@ -13,6 +13,8 @@
#include "eventdispatcher.h"
#include
+#include
+#include
#include
#include
#include
@@ -22,6 +24,9 @@
#include "libstatus.h"
+extern QString getDataStoragePath();
+extern QString getLogFilePath();
+
namespace {
struct RegisterQMLMetaType {
RegisterQMLMetaType() {
@@ -92,7 +97,7 @@ void RCTStatus::startNode(QString configString) {
if (!relativeDataDirPath.startsWith("/"))
relativeDataDirPath.prepend("/");
- QString rootDirPath = getRootDirPath();
+ QString rootDirPath = getDataStoragePath();
QDir rootDir(rootDirPath);
QString absDataDirPath = rootDirPath + relativeDataDirPath;
QDir dataDir(absDataDirPath);
@@ -100,10 +105,11 @@ void RCTStatus::startNode(QString configString) {
dataDir.mkpath(".");
}
+ d_gethLogFilePath = dataDir.absoluteFilePath("geth.log");
configJSON["DataDir"] = absDataDirPath;
configJSON["BackupDisabledDataDir"] = absDataDirPath;
configJSON["KeyStoreDir"] = rootDir.absoluteFilePath("keystore");
- configJSON["LogFile"] = dataDir.absoluteFilePath("geth.log");
+ configJSON["LogFile"] = d_gethLogFilePath;
const QJsonDocument& updatedJsonDoc = QJsonDocument::fromVariant(configJSON);
qCInfo(RCTSTATUS) << "::startNode updated configString: " << updatedJsonDoc.toVariant().toMap();
@@ -141,6 +147,97 @@ void RCTStatus::notifyUsers(QString token, QString payloadJSON, QString tokensJS
}
+#include
+#include
+#include
+#include
+#include
+
+void showFileInGraphicalShell(QWidget *parent, const QFileInfo &fileInfo)
+{
+ // Mac, Windows support folder or file.
+#ifdef Q_OS_WIN
+ const QString explorer = QStandardPaths::findExecutable(QLatin1String("explorer.exe"));
+ if (explorer.isEmpty()) {
+ QMessageBox::warning(parent,
+ QApplication::translate("Core::Internal",
+ "Launching Windows Explorer Failed"),
+ QApplication::translate("Core::Internal",
+ "Could not find explorer.exe in path to launch Windows Explorer."));
+ return;
+ }
+ QStringList param;
+ if (!fileInfo.isDir())
+ param += QLatin1String("/select,");
+ param += QDir::toNativeSeparators(fileInfo.canonicalFilePath());
+ QProcess::startDetached(explorer, param);
+#elif defined(Q_OS_MAC)
+ QStringList scriptArgs;
+ scriptArgs << QLatin1String("-e")
+ << QString::fromLatin1("tell application \"Finder\" to reveal POSIX file \"%1\"")
+ .arg(fileInfo.canonicalFilePath());
+ QProcess::execute(QLatin1String("/usr/bin/osascript"), scriptArgs);
+ scriptArgs.clear();
+ scriptArgs << QLatin1String("-e")
+ << QLatin1String("tell application \"Finder\" to activate");
+ QProcess::execute(QLatin1String("/usr/bin/osascript"), scriptArgs);
+#else
+ // we cannot select a file here, because no file browser really supports it...
+ const QString folder = fileInfo.isDir() ? fileInfo.absoluteFilePath() : fileInfo.dir().absolutePath();
+ QProcess browserProc;
+ browserProc.setProgram("xdg-open");
+ browserProc.setArguments(QStringList(folder));
+ bool success = browserProc.startDetached();
+ const QString error = QString::fromLocal8Bit(browserProc.readAllStandardError());
+ success = success && error.isEmpty();
+ if (!success) {
+ QMessageBox::warning(parent, "Launching Explorer Failed", error);
+ return;
+ }
+#endif
+}
+
+void RCTStatus::sendLogs(QString dbJSON) {
+ Q_D(RCTStatus);
+
+ qCDebug(RCTSTATUS) << "::sendLogs call - logFilePath:" << getLogFilePath()
+ << "d_gethLogFilePath:" << d_gethLogFilePath
+ << "dbJSON:" << dbJSON;
+
+ QString tmpDirName("Status.im");
+ QDir tmpDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation));
+ if (!tmpDir.mkpath(tmpDirName)) {
+ qCWarning(RCTSTATUS) << "::sendLogs could not create temp dir:" << tmpDirName;
+ return;
+ }
+
+ // Check that at least 20MB are available for log generation
+ QStorageInfo storage(tmpDir);
+ if (storage.bytesAvailable() < 20 * 1024 * 1024) {
+ QMessageBox dlg;
+ dlg.warning(QApplication::activeWindow(),
+ "Error", QString("Insufficient storage space available in %1 for log generation. Please free up some space.").arg(storage.rootPath()),
+ QMessageBox::Close);
+ return;
+ }
+
+ QFile zipFile(tmpDir.absoluteFilePath(tmpDirName + QDir::separator() + "Status-debug-logs.zip"));
+ QZipWriter zipWriter(&zipFile);
+ QFile gethLogFile(d_gethLogFilePath);
+ QFile logFile(getLogFilePath());
+ zipWriter.addFile("db.json", dbJSON.toUtf8());
+ if (gethLogFile.exists()) {
+ zipWriter.addFile(QFileInfo(gethLogFile).fileName(), &gethLogFile);
+ }
+ if (logFile.exists()) {
+ zipWriter.addFile(QFileInfo(logFile).fileName(), &logFile);
+ }
+ zipWriter.close();
+
+ showFileInGraphicalShell(QApplication::activeWindow(), QFileInfo(zipFile));
+}
+
+
void RCTStatus::addPeer(QString enode, double callbackId) {
Q_D(RCTStatus);
qCDebug(RCTSTATUS) << "::addPeer call - callbackId:" << callbackId;
@@ -177,7 +274,7 @@ void RCTStatus::verify(QString address, QString password, double callbackId) {
Q_D(RCTStatus);
qCInfo(RCTSTATUS) << "::verify call - callbackId:" << callbackId;
QtConcurrent::run([&](QString address, QString password, double callbackId) {
- QDir rootDir(getRootDirPath());
+ QDir rootDir(getDataStoragePath());
QString keystorePath = rootDir.absoluteFilePath("keystore");
const char* result = VerifyAccountPassword(keystorePath.toUtf8().data(), address.toUtf8().data(), password.toUtf8().data());
logStatusGoResult("::verify VerifyAccountPassword", result);
@@ -334,17 +431,3 @@ void RCTStatus::updateMailservers(QString enodes, double callbackId) {
d->bridge->invokePromiseCallback(callbackId, QVariantList{result});
}, enodes, callbackId);
}
-
-QString RCTStatus::getRootDirPath() {
- QString statusDataDir = qgetenv("STATUS_DATA_DIR");
- QString rootDirPath;
- if (!statusDataDir.isEmpty()) {
- rootDirPath = statusDataDir;
- }
- else {
- rootDirPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
- }
-
- return rootDirPath;
-}
-
diff --git a/modules/react-native-status/desktop/rctstatus.h b/modules/react-native-status/desktop/rctstatus.h
index c33b75f142..3713fe4e98 100644
--- a/modules/react-native-status/desktop/rctstatus.h
+++ b/modules/react-native-status/desktop/rctstatus.h
@@ -39,6 +39,7 @@ public:
Q_INVOKABLE void stopNode();
Q_INVOKABLE void createAccount(QString password, double callbackId);
Q_INVOKABLE void notifyUsers(QString token, QString payloadJSON, QString tokensJSON, double callbackId);
+ Q_INVOKABLE void sendLogs(QString dbJSON);
Q_INVOKABLE void addPeer(QString enode, double callbackId);
Q_INVOKABLE void recoverAccount(QString passphrase, QString password, double callbackId);
Q_INVOKABLE void login(QString address, QString password, double callbackId);
@@ -75,9 +76,9 @@ private Q_SLOTS:
private:
void logStatusGoResult(const char* methodName, const char* result);
- QString getRootDirPath();
QScopedPointer d_ptr;
+ QString d_gethLogFilePath;
};
#endif // RCTSTATUS_H
diff --git a/modules/react-native-status/ios/RCTStatus/RCTStatus.m b/modules/react-native-status/ios/RCTStatus/RCTStatus.m
index 7dc3e6d06b..a5c6d308ed 100644
--- a/modules/react-native-status/ios/RCTStatus/RCTStatus.m
+++ b/modules/react-native-status/ios/RCTStatus/RCTStatus.m
@@ -196,6 +196,16 @@ RCT_EXPORT_METHOD(notifyUsers:(NSString *)message
#endif
}
+////////////////////////////////////////////////////////////////////
+#pragma mark - SendLogs method
+//////////////////////////////////////////////////////////////////// sendLogs
+RCT_EXPORT_METHOD(sendLogs:(NSString *)dbJson) {
+ // TODO: Implement SendLogs for iOS
+#if DEBUG
+ NSLog(@"SendLogs() method called, not implemented");
+#endif
+}
+
//////////////////////////////////////////////////////////////////// addPeer
RCT_EXPORT_METHOD(addPeer:(NSString *)enode
callback:(RCTResponseSenderBlock)callback) {
diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs
index 46c831baf9..575004143b 100644
--- a/src/status_im/events.cljs
+++ b/src/status_im/events.cljs
@@ -24,6 +24,7 @@
[status-im.hardwallet.core :as hardwallet]
[status-im.i18n :as i18n]
[status-im.init.core :as init]
+ [status-im.utils.logging.core :as logging]
[status-im.log-level.core :as log-level]
[status-im.mailserver.core :as mailserver]
[status-im.network.core :as network]
@@ -543,6 +544,13 @@
(fn [cofx [_ url data modal?]]
(extensions.registry/install cofx url data modal?)))
+;; logging module
+
+(handlers/register-handler-fx
+ :logging.ui/send-logs-pressed
+ (fn [cofx _]
+ (logging/send-logs cofx)))
+
;; log-level module
(handlers/register-handler-fx
diff --git a/src/status_im/log_level/core.cljs b/src/status_im/log_level/core.cljs
index 1d68e1829f..52df58216b 100644
--- a/src/status_im/log_level/core.cljs
+++ b/src/status_im/log_level/core.cljs
@@ -2,17 +2,20 @@
(:require [re-frame.core :as re-frame]
[status-im.accounts.update.core :as accounts.update]
[status-im.react-native.js-dependencies :as rn-dependencies]
+ [status-im.node.core :as node]
[status-im.i18n :as i18n]
[status-im.utils.fx :as fx]))
(fx/defn save-log-level
[{:keys [db now] :as cofx} log-level]
- (let [settings (get-in db [:account/account :settings])]
+ (let [settings (get-in db [:account/account :settings])
+ new-settings (if log-level
+ (assoc settings :log-level log-level)
+ (dissoc settings :log-level))]
(accounts.update/update-settings cofx
- (if log-level
- (assoc settings :log-level log-level)
- (dissoc settings :log-level))
- {:success-event [:accounts.update.callback/save-settings-success]})))
+ new-settings
+ (when (not= (node/get-log-level settings) (node/get-log-level new-settings))
+ {:success-event [:accounts.update.callback/save-settings-success]}))))
(fx/defn show-change-log-level-confirmation
[{:keys [db]} {:keys [name value] :as log-level}]
diff --git a/src/status_im/native_module/core.cljs b/src/status_im/native_module/core.cljs
index 180eac5876..8839b6ab64 100644
--- a/src/status_im/native_module/core.cljs
+++ b/src/status_im/native_module/core.cljs
@@ -54,6 +54,9 @@
(defn notify-users [m callback]
(native-module/notify-users m callback))
+(defn send-logs [dbJson]
+ (native-module/send-logs dbJson))
+
(defn add-peer [enode callback]
(native-module/add-peer enode callback))
diff --git a/src/status_im/native_module/impl/module.cljs b/src/status_im/native_module/impl/module.cljs
index d10314966d..c81610d59a 100644
--- a/src/status_im/native_module/impl/module.cljs
+++ b/src/status_im/native_module/impl/module.cljs
@@ -87,6 +87,10 @@
(when status
(call-module #(.notifyUsers status message payload tokens on-result))))
+(defn send-logs [dbJson]
+ (when status
+ (call-module #(.sendLogs status dbJson))))
+
(defn add-peer [enode on-result]
(when (and @node-started status)
(call-module #(.addPeer status enode on-result))))
diff --git a/src/status_im/node/core.cljs b/src/status_im/node/core.cljs
index 2318d8ffff..7ecbeb545e 100644
--- a/src/status_im/node/core.cljs
+++ b/src/status_im/node/core.cljs
@@ -72,6 +72,12 @@
[limit nodes]
(take limit (shuffle nodes)))
+(defn get-log-level
+ [account-settings]
+ (or (:log-level account-settings)
+ (if utils.platform/desktop? ""
+ config/log-level-status-go)))
+
(defn- get-account-node-config [db address]
(let [accounts (get db :accounts/accounts)
current-fleet-key (fleet/current-fleet db address)
@@ -85,9 +91,7 @@
:installation-id (get db :accounts/new-installation-id)}
(get accounts address))
use-custom-bootnodes (get-in settings [:bootnodes network])
- log-level (or (:log-level settings)
- (if utils.platform/desktop? ""
- config/log-level-status-go))]
+ log-level (get-log-level settings)]
(cond-> (get-in networks [network :config])
:always
(get-base-node-config)
diff --git a/src/status_im/ui/screens/desktop/main/tabs/profile/styles.cljs b/src/status_im/ui/screens/desktop/main/tabs/profile/styles.cljs
index 31fcac0137..2296737090 100644
--- a/src/status_im/ui/screens/desktop/main/tabs/profile/styles.cljs
+++ b/src/status_im/ui/screens/desktop/main/tabs/profile/styles.cljs
@@ -183,6 +183,19 @@
:margin-bottom 8
:background-color colors/gray-light})
+(defn adv-settings-row-text [color]
+ {:color color
+ :font-size 14})
+
+(defn adv-settings-row [active?]
+ {:justify-content :space-between
+ :flex-direction :row
+ :padding-horizontal 24
+ :height 56
+ :align-self :stretch
+ :align-items :center
+ :background-color (if active? colors/gray-lighter colors/white)})
+
(def pair-button
{:margin-left 32})
diff --git a/src/status_im/ui/screens/desktop/main/tabs/profile/views.cljs b/src/status_im/ui/screens/desktop/main/tabs/profile/views.cljs
index 6171e519b3..69bad0bec1 100644
--- a/src/status_im/ui/screens/desktop/main/tabs/profile/views.cljs
+++ b/src/status_im/ui/screens/desktop/main/tabs/profile/views.cljs
@@ -7,6 +7,8 @@
[status-im.utils.utils :as utils]
[status-im.ui.components.colors :as colors]
[status-im.i18n :as i18n]
+ [status-im.utils.logging.core :as logging]
+ [status-im.utils.platform :as platform]
[status-im.ui.components.icons.vector-icons :as vector-icons]
[taoensso.timbre :as log]
[status-im.utils.gfycat.core :as gfy]
@@ -135,15 +137,21 @@
(views/defview logging-display []
(views/letsubs [logging-enabled [:settings/logging-enabled]]
- [react/view {:style (styles/profile-row false)}
- [react/text {:style (assoc (styles/profile-row-text colors/black)
- :font-size 14)} (i18n/label :t/logging-enabled)]
- [react/switch {:on-tint-color colors/blue
- :value logging-enabled
- :on-value-change #(re-frame/dispatch [:log-level.ui/logging-enabled (not logging-enabled)])}]]))
+ [react/view
+ [react/view {:style (styles/adv-settings-row false)}
+ [react/text {:style (assoc (styles/adv-settings-row-text colors/black)
+ :font-size 14)} (i18n/label :t/logging-enabled)]
+ [react/switch {:on-tint-color colors/blue
+ :value logging-enabled
+ :on-value-change #(re-frame/dispatch [:log-level.ui/logging-enabled (not logging-enabled)])}]]
+ [react/view {:style (styles/adv-settings-row false)}
+ [react/touchable-highlight {:on-press #(re-frame/dispatch [:logging.ui/send-logs-pressed])}
+ [react/text {:style (styles/adv-settings-row-text colors/red)}
+ (i18n/label :t/send-logs)]]]]))
(views/defview advanced-settings []
- (views/letsubs [current-mailserver-id [:mailserver/current-id]
+ (views/letsubs [installations [:pairing/installations]
+ current-mailserver-id [:mailserver/current-id]
mailservers [:mailserver/fleet-mailservers]
mailserver-state [:mailserver/state]
node-status [:node-status]
@@ -197,21 +205,21 @@
[vector-icons/icon :icons/qr {:style {:tint-color colors/blue}}]]]])
(defn help-item [help-open?]
- [react/touchable-highlight {:style (styles/profile-row help-open?)
+ [react/touchable-highlight {:style (styles/adv-settings-row help-open?)
:on-press #(re-frame/dispatch [:navigate-to (if help-open? :home :help-center)])}
[react/view {:style styles/adv-settings}
- [react/text {:style (styles/profile-row-text colors/black)
+ [react/text {:style (styles/adv-settings-row-text colors/black)
:font (if help-open? :medium :default)}
(i18n/label :t/help-center)]
[vector-icons/icon :icons/forward {:style {:tint-color colors/gray}}]]])
(defn advanced-settings-item [adv-settings-open?]
- [react/touchable-highlight {:style (styles/profile-row adv-settings-open?)
+ [react/touchable-highlight {:style (styles/adv-settings-row adv-settings-open?)
:on-press #(do
(re-frame/dispatch [:navigate-to (if adv-settings-open? :home :advanced-settings)])
(re-frame/dispatch [:load-debug-metrics]))}
[react/view {:style styles/adv-settings}
- [react/text {:style (styles/profile-row-text colors/black)
+ [react/text {:style (styles/adv-settings-row-text colors/black)
:font (if adv-settings-open? :medium :default)}
(i18n/label :t/advanced-settings)]
[vector-icons/icon :icons/forward {:style {:tint-color colors/gray}}]]])
diff --git a/src/status_im/ui/screens/profile/user/styles.cljs b/src/status_im/ui/screens/profile/user/styles.cljs
index 5b47fc61d8..102d5d39fe 100644
--- a/src/status_im/ui/screens/profile/user/styles.cljs
+++ b/src/status_im/ui/screens/profile/user/styles.cljs
@@ -38,6 +38,14 @@
(defstyle my-profile-settings-logout
{:min-width "50%"})
+(defstyle my-profile-settings-send-logs-wrapper
+ {:flex-direction :row
+ :justify-content :space-between
+ :align-items :center})
+
+(defstyle my-profile-settings-send-logs
+ {:min-width "50%"})
+
(def advanced-button
{:margin-top 16
:margin-bottom 12})
diff --git a/src/status_im/ui/screens/profile/user/views.cljs b/src/status_im/ui/screens/profile/user/views.cljs
index 43008f9af8..a52735ef5d 100644
--- a/src/status_im/ui/screens/profile/user/views.cljs
+++ b/src/status_im/ui/screens/profile/user/views.cljs
@@ -170,6 +170,13 @@
{:label-kw :t/log-level
:action-fn #(re-frame/dispatch [:navigate-to :log-level-settings])
:accessibility-label :log-level-settings-button}]
+ (when (and dev-mode? (not platform/ios?))
+ [react/view styles/my-profile-settings-send-logs-wrapper
+ [react/view styles/my-profile-settings-send-logs
+ [profile.components/settings-item {:label-kw :t/send-logs
+ :destructive? true
+ :hide-arrow? true
+ :action-fn #(re-frame/dispatch [:logging.ui/send-logs-pressed])}]]])
[profile.components/settings-item-separator]
[profile.components/settings-item
{:label-kw :t/fleet
@@ -250,7 +257,7 @@
{:contact current-account
:source :public-key
:value public-key}])
- :style styles/share-contact-code-button
+ :style styles/share-contact-code-button
:accessibility-label :share-my-profile-button}
(i18n/label :t/share-my-profile)]]
[react/view styles/my-profile-info-container
diff --git a/src/status_im/utils/logging/core.cljs b/src/status_im/utils/logging/core.cljs
new file mode 100644
index 0000000000..745b835b9f
--- /dev/null
+++ b/src/status_im/utils/logging/core.cljs
@@ -0,0 +1,36 @@
+(ns status-im.utils.logging.core
+ (:require [re-frame.core :as re-frame]
+ [status-im.native-module.core :as status]
+ [status-im.utils.fx :as fx]
+ [status-im.utils.types :as types]))
+
+(fx/defn send-logs
+ [{:keys [db] :as cofx}]
+ ;; TODO: Add message explaining db export
+ (let [db-json (types/clj->json (select-keys db [:app-state
+ :current-chat-id
+ :discover-current-dapp
+ :discover-search-tags
+ :discoveries
+ :initial-props
+ :keyboard-height
+ :keyboard-max-height
+ :navigation-stack
+ :network
+ :network-status
+ :peers-count
+ :peers-summary
+ :status-module-initialized?
+ :sync-state
+ :tab-bar-visible?
+ :view-id
+ :chat/cooldown-enabled?
+ :chat/cooldowns
+ :chat/last-outgoing-message-sent-at
+ :chat/spam-messages-frequency
+ :chats/loading?
+ :desktop/desktop
+ :dimensions/window
+ :my-profile/editing?
+ :node/status]))]
+ (status/send-logs db-json)))
diff --git a/translations/en.json b/translations/en.json
index ef17b95eef..7784a63937 100644
--- a/translations/en.json
+++ b/translations/en.json
@@ -261,6 +261,7 @@
"logout": "Log out",
"status-not-sent-tap": "Not confirmed. Tap for options",
"status-not-sent-click": "Not confirmed. Click for options",
+ "send-logs": "Send logs",
"edit-network-config": "Edit network config",
"clear-history-confirmation": "Clear history?",
"connect": "Connect",