Remove Start chat fragment, replaced with EditTextDialog (Floating Action Button)

This commit is contained in:
ChronosX88 2019-04-29 20:44:00 +04:00
parent 6181665938
commit 58ed8aef78
14 changed files with 242 additions and 293 deletions

View File

@ -46,11 +46,13 @@ interface CoreContracts {
interface IMainLogicContract {
fun initPeer()
fun sendStartChatMessage(username: String)
fun shutdownPeer()
}
interface IMainPresenterContract {
fun initPeer()
fun startChatWithPeer(username: String)
fun onDestroy()
}

View File

@ -22,11 +22,14 @@ import net.tomp2p.nat.PeerBuilderNAT;
import net.tomp2p.nat.PeerNAT;
import net.tomp2p.p2p.PeerBuilder;
import net.tomp2p.peers.Number160;
import net.tomp2p.peers.Number640;
import net.tomp2p.peers.PeerAddress;
import net.tomp2p.relay.tcp.TCPRelayClientConfig;
import net.tomp2p.replication.IndirectReplication;
import net.tomp2p.storage.Data;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
import java.net.Inet4Address;
@ -34,6 +37,8 @@ import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
@ -42,10 +47,14 @@ import io.github.chronosx88.influence.contracts.CoreContracts;
import io.github.chronosx88.influence.helpers.AppHelper;
import io.github.chronosx88.influence.helpers.JVMShutdownHook;
import io.github.chronosx88.influence.helpers.KeyPairManager;
import io.github.chronosx88.influence.helpers.LocalDBWrapper;
import io.github.chronosx88.influence.helpers.NetworkHandler;
import io.github.chronosx88.influence.helpers.ObservableUtils;
import io.github.chronosx88.influence.helpers.P2PUtils;
import io.github.chronosx88.influence.helpers.StorageBerkeleyDB;
import io.github.chronosx88.influence.helpers.actions.UIActions;
import io.github.chronosx88.influence.models.ChatMetadata;
import io.github.chronosx88.influence.models.NewChatRequestMessage;
import io.github.chronosx88.influence.models.PublicUserProfile;
public class MainLogic implements CoreContracts.IMainLogicContract {
@ -236,8 +245,10 @@ public class MainLogic implements CoreContracts.IMainLogicContract {
if(replication != null) {
replication.shutdown();
}
peerDHT.peer().announceShutdown().start().awaitUninterruptibly();
peerDHT.peer().shutdown().awaitUninterruptibly();
if(peerDHT != null) {
peerDHT.peer().announceShutdown().start().awaitUninterruptibly();
peerDHT.peer().shutdown().awaitUninterruptibly();
}
storage.close();
System.exit(0);
}).start();
@ -292,4 +303,74 @@ public class MainLogic implements CoreContracts.IMainLogicContract {
channelServerConfiguration.byteBufPool(false);
return channelServerConfiguration;
}
@Override
public void sendStartChatMessage(@NotNull String username) {
if(AppHelper.getPeerDHT() == null) {
ObservableUtils.notifyUI(UIActions.NODE_IS_OFFLINE);
return;
}
String companionPeerID = getPeerIDByUsername(username);
if(companionPeerID == null) {
ObservableUtils.notifyUI(UIActions.PEER_NOT_EXIST);
return;
}
PublicUserProfile recipientPublicProfile = getPublicProfile(companionPeerID);
if(recipientPublicProfile == null) {
ObservableUtils.notifyUI(UIActions.PEER_NOT_EXIST);
return;
}
NewChatRequestMessage newChatRequestMessage = new NewChatRequestMessage(UUID.randomUUID().toString(), UUID.randomUUID().toString(), AppHelper.getPeerID(), AppHelper.getUsername(), System.currentTimeMillis(), 0);
try {
if(P2PUtils.put(companionPeerID + "_pendingChats", newChatRequestMessage.getChatID(), new Data(gson.toJson(newChatRequestMessage)))) {
Log.i(LOG_TAG, "# Create new offline chat request is successful! ChatID: " + newChatRequestMessage.getChatID());
} else {
Log.e(LOG_TAG, "# Failed to create offline chat request. ChatID: " + newChatRequestMessage.getChatID());
}
} catch (IOException e) {
e.printStackTrace();
}
ArrayList<String> admins = new ArrayList<>();
admins.add(AppHelper.getPeerID());
Data data = null;
try {
data = new Data(gson.toJson(new ChatMetadata(username, admins, new ArrayList<>())));
} catch (IOException e) {
e.printStackTrace();
}
data.protectEntry(keyPairManager.openMainKeyPair());
P2PUtils.put(newChatRequestMessage.getChatID() + "_metadata", null, data);
LocalDBWrapper.createChatEntry(newChatRequestMessage.getChatID(), username, newChatRequestMessage.getChatID() + "_metadata", newChatRequestMessage.getChatID() + "_members", 0);
ObservableUtils.notifyUI(UIActions.NEW_CHAT);
}
private PublicUserProfile getPublicProfile(String peerID) {
PublicUserProfile publicProfile = null;
Map<Number640, Data> data = P2PUtils.get(peerID + "_profile");
if (data != null && data.size() == 1) {
try {
publicProfile = gson.fromJson((String) data.values().iterator().next().object(), PublicUserProfile.class);
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
return publicProfile;
}
return null;
}
private String getPeerIDByUsername(String username) {
Map<Number640, Data> usernameMap = P2PUtils.get(username);
if(usernameMap == null) {
return null;
}
try {
return (String) usernameMap.values().iterator().next().object();
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
return null;
}
}

View File

@ -1,110 +0,0 @@
package io.github.chronosx88.influence.logic;
import android.util.Log;
import com.google.gson.Gson;
import net.tomp2p.dht.PeerDHT;
import net.tomp2p.peers.Number640;
import net.tomp2p.storage.Data;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Map;
import java.util.UUID;
import io.github.chronosx88.influence.contracts.CoreContracts;
import io.github.chronosx88.influence.helpers.AppHelper;
import io.github.chronosx88.influence.helpers.KeyPairManager;
import io.github.chronosx88.influence.helpers.LocalDBWrapper;
import io.github.chronosx88.influence.helpers.ObservableUtils;
import io.github.chronosx88.influence.helpers.P2PUtils;
import io.github.chronosx88.influence.helpers.actions.UIActions;
import io.github.chronosx88.influence.models.ChatMetadata;
import io.github.chronosx88.influence.models.NewChatRequestMessage;
import io.github.chronosx88.influence.models.PublicUserProfile;
public class StartChatLogic implements CoreContracts.IStartChatLogicContract {
private PeerDHT peerDHT;
private Gson gson;
private KeyPairManager keyPairManager;
private final static String LOG_TAG = "StartChatLogic";
public StartChatLogic() {
peerDHT = AppHelper.getPeerDHT();
gson = new Gson();
keyPairManager = new KeyPairManager();
}
@Override
public void sendStartChatMessage(String username) {
if(peerDHT == null) {
ObservableUtils.notifyUI(UIActions.NODE_IS_OFFLINE);
return;
}
new Thread(() -> {
String peerID = getPeerIDByUsername(username);
if(peerID == null) {
ObservableUtils.notifyUI(UIActions.PEER_NOT_EXIST);
return;
}
PublicUserProfile recipientPublicProfile = getPublicProfile(peerID);
if(recipientPublicProfile == null) {
ObservableUtils.notifyUI(UIActions.PEER_NOT_EXIST);
return;
}
NewChatRequestMessage newChatRequestMessage = new NewChatRequestMessage(UUID.randomUUID().toString(), UUID.randomUUID().toString(), AppHelper.getPeerID(), AppHelper.getUsername(), System.currentTimeMillis(), 0);
try {
if(P2PUtils.put(peerID + "_pendingChats", newChatRequestMessage.getChatID(), new Data(gson.toJson(newChatRequestMessage)))) {
Log.i(LOG_TAG, "# Create new offline chat request is successful! ChatID: " + newChatRequestMessage.getChatID());
} else {
Log.e(LOG_TAG, "# Failed to create offline chat request. ChatID: " + newChatRequestMessage.getChatID());
}
} catch (IOException e) {
e.printStackTrace();
}
ArrayList<String> admins = new ArrayList<>();
admins.add(AppHelper.getPeerID());
Data data = null;
try {
data = new Data(gson.toJson(new ChatMetadata(username, admins, new ArrayList<>())));
} catch (IOException e) {
e.printStackTrace();
}
data.protectEntry(keyPairManager.openMainKeyPair());
P2PUtils.put(newChatRequestMessage.getChatID() + "_metadata", null, data);
LocalDBWrapper.createChatEntry(newChatRequestMessage.getChatID(), username, newChatRequestMessage.getChatID() + "_metadata", newChatRequestMessage.getChatID() + "_members", 0);
ObservableUtils.notifyUI(UIActions.NEW_CHAT);
}).start();
}
private PublicUserProfile getPublicProfile(String peerID) {
PublicUserProfile publicProfile = null;
Map<Number640, Data> data = P2PUtils.get(peerID + "_profile");
if (data != null && data.size() == 1) {
try {
publicProfile = gson.fromJson((String) data.values().iterator().next().object(), PublicUserProfile.class);
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
return publicProfile;
}
return null;
}
private String getPeerIDByUsername(String username) {
Map<Number640, Data> usernameMap = P2PUtils.get(username);
if(usernameMap == null) {
return null;
}
try {
return (String) usernameMap.values().iterator().next().object();
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
return null;
}
}

View File

@ -1,13 +1,21 @@
package io.github.chronosx88.influence.presenters
import com.google.gson.JsonObject
import io.github.chronosx88.influence.R
import io.github.chronosx88.influence.contracts.CoreContracts
import io.github.chronosx88.influence.contracts.observer.IObserver
import io.github.chronosx88.influence.helpers.AppHelper
import io.github.chronosx88.influence.helpers.actions.UIActions
import io.github.chronosx88.influence.logic.MainLogic
import org.jetbrains.anko.doAsync
class MainPresenter(private val view: CoreContracts.IMainViewContract) : CoreContracts.IMainPresenterContract {
class MainPresenter(private val view: CoreContracts.IMainViewContract) : CoreContracts.IMainPresenterContract, IObserver {
private val logic: CoreContracts.IMainLogicContract = MainLogic()
init {
AppHelper.getObservable().register(this)
}
override fun initPeer() {
if (AppHelper.getPeerDHT() == null) {
logic.initPeer()
@ -17,6 +25,31 @@ class MainPresenter(private val view: CoreContracts.IMainViewContract) : CoreCon
}
}
override fun startChatWithPeer(username: String) {
doAsync {
logic.sendStartChatMessage(username)
}
}
override fun handleEvent(obj: JsonObject) {
when(obj.get("action").asInt) {
UIActions.PEER_NOT_EXIST -> {
view.showProgressBar(false)
view.showSnackbar("Данный узел не существует!")
}
UIActions.NEW_CHAT -> {
view.showProgressBar(false)
view.showSnackbar("Чат успешно создан!")
}
UIActions.NODE_IS_OFFLINE -> {
view.showProgressBar(false)
view.showSnackbar("Нода не запущена!")
}
}
}
override fun onDestroy() {
logic.shutdownPeer()
}

View File

@ -1,49 +0,0 @@
package io.github.chronosx88.influence.presenters;
import com.google.gson.JsonObject;
import io.github.chronosx88.influence.contracts.CoreContracts;
import io.github.chronosx88.influence.contracts.observer.IObserver;
import io.github.chronosx88.influence.helpers.AppHelper;
import io.github.chronosx88.influence.helpers.actions.UIActions;
import io.github.chronosx88.influence.logic.StartChatLogic;
public class StartChatPresenter implements CoreContracts.IStartChatPresenterContract, IObserver {
private CoreContracts.IStartChatViewContract view;
private CoreContracts.IStartChatLogicContract logic;
public StartChatPresenter(CoreContracts.IStartChatViewContract view) {
this.view = view;
this.logic = new StartChatLogic();
AppHelper.getObservable().register(this);
}
@Override
public void startChatWithPeer(String peerID) {
view.showProgressDialog(true);
logic.sendStartChatMessage(peerID);
}
@Override
public void handleEvent(JsonObject object) {
switch (object.get("action").getAsInt()) {
case UIActions.PEER_NOT_EXIST: {
view.showProgressDialog(false);
view.showMessage("Данный узел не существует!");
break;
}
case UIActions.NEW_CHAT: {
view.showProgressDialog(false);
view.showMessage("Чат успешно создан!");
break;
}
case UIActions.NODE_IS_OFFLINE: {
view.showProgressDialog(false);
view.showMessage("Нода не запущена!");
break;
}
}
}
}

View File

@ -6,13 +6,16 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.Toast;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import com.google.gson.JsonObject;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
@ -27,7 +30,7 @@ import io.github.chronosx88.influence.helpers.actions.UIActions;
import io.github.chronosx88.influence.presenters.MainPresenter;
import io.github.chronosx88.influence.views.fragments.ChatListFragment;
import io.github.chronosx88.influence.views.fragments.SettingsFragment;
import io.github.chronosx88.influence.views.fragments.StartChatFragment;
import kotlin.Pair;
public class MainActivity extends AppCompatActivity implements IObserver, CoreContracts.IMainViewContract {
@ -48,9 +51,6 @@ public class MainActivity extends AppCompatActivity implements IObserver, CoreCo
case R.id.action_settings:
selectedFragment = new SettingsFragment();
break;
case R.id.action_start_chat:
selectedFragment = new StartChatFragment();
break;
}
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
@ -69,6 +69,19 @@ public class MainActivity extends AppCompatActivity implements IObserver, CoreCo
BottomNavigationView navigation = findViewById(R.id.main_bottom_navigation);
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener);
FloatingActionButton fab = findViewById(R.id.add_chat);
fab.setOnClickListener((v) -> {
Pair<AlertDialog.Builder, EditText> pair = ViewUtils.INSTANCE.setupEditTextDialog(MainActivity.this, getString(R.string.input_companion_username));
pair.getFirst().setPositiveButton(getString(R.string.ok), (dialog, which) -> {
progressDialog.show();
presenter.startChatWithPeer(pair.getSecond().getText().toString());
});
pair.getFirst().setNegativeButton(getString(R.string.cancel), (dialog, which) -> {
dialog.cancel();
});
pair.getFirst().show();
});
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.main_fragment_container, new ChatListFragment())
@ -155,17 +168,21 @@ public class MainActivity extends AppCompatActivity implements IObserver, CoreCo
@Override
public void showSnackbar(@NotNull String message) {
Snackbar.make(getRootView(), message, Snackbar.LENGTH_LONG)
.show();
runOnUiThread(() -> {
Snackbar.make(getRootView(), message, Snackbar.LENGTH_LONG)
.show();
});
}
@Override
public void showProgressBar(boolean state) {
if(state) {
progressDialog.show();
} else {
progressDialog.dismiss();
}
runOnUiThread(() -> {
if(state) {
progressDialog.show();
} else {
progressDialog.dismiss();
}
});
}
private View getRootView() {

View File

@ -0,0 +1,42 @@
/*
* Copyright (C) 2019 ChronosX88
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.chronosx88.influence.views
import android.content.Context
import android.widget.EditText
import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog
import io.github.chronosx88.influence.R
object ViewUtils {
fun setupEditTextDialog(context: Context, message: String): Pair<AlertDialog.Builder, EditText> {
val alertDialog = AlertDialog.Builder(context)
alertDialog.setTitle(message)
val input = EditText(context)
val lp = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT)
input.setSingleLine()
input.layoutParams = lp
alertDialog.setView(input)
return Pair(alertDialog, input)
}
}

View File

@ -1,69 +0,0 @@
package io.github.chronosx88.influence.views.fragments;
import android.app.ProgressDialog;
import android.os.Bundle;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.textfield.TextInputLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.fragment.app.Fragment;
import io.github.chronosx88.influence.R;
import io.github.chronosx88.influence.contracts.CoreContracts;
import io.github.chronosx88.influence.presenters.StartChatPresenter;
public class StartChatFragment extends Fragment implements CoreContracts.IStartChatViewContract {
private TextInputLayout textInputPeerID;
private ProgressDialog progressDialog;
private Button createChatButton;
private StartChatPresenter presenter;
private Handler mainThreadHandler;
private CoordinatorLayout coordinatorLayout;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.start_chat_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
presenter = new StartChatPresenter(this);
textInputPeerID = view.findViewById(R.id.textInputPeerID);
progressDialog = new ProgressDialog(getActivity(), R.style.AlertDialogTheme);
progressDialog.setCancelable(false);
progressDialog.setProgressStyle(android.R.style.Widget_ProgressBar_Small);
createChatButton = view.findViewById(R.id.create_chat_button);
createChatButton.setOnClickListener((v) -> {
presenter.startChatWithPeer(textInputPeerID.getEditText().getText().toString());
});
mainThreadHandler = new Handler(getContext().getMainLooper());
coordinatorLayout = getView().findViewById(R.id.start_chat_coordinator);
}
@Override
public void showMessage(String message) {
mainThreadHandler.post(() -> {
Snackbar.make(coordinatorLayout, message, Snackbar.LENGTH_SHORT).show();
});
}
@Override
public void showProgressDialog(boolean enabled) {
mainThreadHandler.post(() -> {
if(enabled) {
progressDialog.show();
} else {
progressDialog.dismiss();
}
});
}
}

View File

@ -0,0 +1,22 @@
<!--
~ Copyright (C) 2019 ChronosX88
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@ -7,18 +7,33 @@
tools:context=".views.MainActivity"
android:orientation="vertical">
<FrameLayout
android:id="@+id/main_fragment_container"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="10"/>
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/main_fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="10"/>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/main_bottom_navigation"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@android:color/white"
app:menu="@menu/main_bottom_nav_menu"
android:layout_weight="1"
android:layout_gravity="bottom"/>
</LinearLayout>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/main_bottom_navigation"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@android:color/white"
app:menu="@menu/main_bottom_nav_menu"
android:layout_weight="1"
android:layout_gravity="bottom"/>
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/add_chat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="68dp"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_add_white_24dp"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/start_chat_coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputPeerID"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:hint="@string/username_hint"/>
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/create_chat_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Создать чат"/>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -8,8 +8,4 @@
android:id="@+id/action_settings"
android:icon="@drawable/ic_settings_white"
android:title="Настройки" />
<item
android:id="@+id/action_start_chat"
android:icon="@drawable/ic_person_add_white"
android:title="Начать чат" />
</menu>

View File

@ -9,4 +9,5 @@
<string name="username_hint">Имя пользователя</string>
<string name="reconnect_network">Переподключиться к сети</string>
<string name="node_already_running">Узел уже запущен</string>
<string name="input_companion_username">Введите имя пользователя собеседника</string>
</resources>

View File

@ -8,4 +8,5 @@
<string name="username_hint">Username</string>
<string name="reconnect_network">Reconnect to the network</string>
<string name="node_already_running">Node already running</string>
<string name="input_companion_username">Input interlocutor\'s username</string>
</resources>