diff --git a/CHANGELOG.md b/CHANGELOG.md
index a3028bde0..f2da0e862 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,7 @@
* Avoid gray avatar on profile creation
* Avoid last item in chat list being covered by the floating button
* Add disk usage statistics to log
+* New experimental feature: several addresses per profile
* Update to core 2.33.0
## v2.25.0
diff --git a/flake.nix b/flake.nix
index 5ab488602..de876b7f8 100644
--- a/flake.nix
+++ b/flake.nix
@@ -18,7 +18,7 @@
build-tools-35-0-0
cmdline-tools-latest
platform-tools
- platforms-android-35
+ platforms-android-36
ndk-27-2-12479018
]);
rust-version = pkgs.lib.removeSuffix "\n"
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index b33753672..37c278ba4 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -234,13 +234,17 @@
android:theme="@style/TextSecure.LightNoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
-
+
+
{
- startRegistrationActivity();
+ startActivity(new Intent(this, EditRelayActivity.class));
signUpDialog.dismiss();
});
view.findViewById(R.id.scan_qr_button).setOnClickListener((v) -> {
@@ -367,11 +368,6 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
signUpDialog.show();
}
- private void startRegistrationActivity() {
- Intent intent = new Intent(this, EditTransportActivity.class);
- startActivity(intent);
- }
-
private void updateProvider() {
if (isDcLogin) {
signUpBtn.setText(R.string.login_title);
diff --git a/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPreferenceFragment.java b/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPreferenceFragment.java
index 1432bc852..5387114b0 100644
--- a/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPreferenceFragment.java
+++ b/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPreferenceFragment.java
@@ -7,9 +7,7 @@ import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_MVBOX_MOVE;
import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_ONLY_FETCH_MVBOX;
import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_SHOW_EMAILS;
import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_WEBXDC_REALTIME_ENABLED;
-import static org.thoughtcrime.securesms.connect.DcHelper.getRpc;
-import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
@@ -17,7 +15,6 @@ import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
-import android.webkit.WebView;
import android.widget.EditText;
import android.widget.Toast;
@@ -29,12 +26,10 @@ import androidx.preference.ListPreference;
import androidx.preference.Preference;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
-import org.thoughtcrime.securesms.ConversationActivity;
import org.thoughtcrime.securesms.LogViewActivity;
import org.thoughtcrime.securesms.R;
-import org.thoughtcrime.securesms.EditTransportActivity;
+import org.thoughtcrime.securesms.relay.RelayListActivity;
import org.thoughtcrime.securesms.connect.DcEventCenter;
-import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.proxy.ProxySettingsActivity;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.ScreenLockUtil;
@@ -48,8 +43,6 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.util.Objects;
-import chat.delta.rpc.RpcException;
-
public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
implements DcEventCenter.DcEventDelegate
@@ -207,12 +200,12 @@ public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
});
}
- Preference passwordAndAccount = this.findPreference("password_account_settings_button");
- if (passwordAndAccount != null) {
- passwordAndAccount.setOnPreferenceClickListener(((preference) -> {
- boolean result = ScreenLockUtil.applyScreenLock(requireActivity(), getString(R.string.edit_transport), getString(R.string.enter_system_secret_to_continue), REQUEST_CODE_CONFIRM_CREDENTIALS_ACCOUNT);
+ Preference relayListBtn = this.findPreference("pref_relay_list_button");
+ if (relayListBtn != null) {
+ relayListBtn.setOnPreferenceClickListener(((preference) -> {
+ boolean result = ScreenLockUtil.applyScreenLock(requireActivity(), getString(R.string.transports), getString(R.string.enter_system_secret_to_continue), REQUEST_CODE_CONFIRM_CREDENTIALS_ACCOUNT);
if (!result) {
- openRegistrationActivity();
+ openRelayListActivity();
}
return true;
}));
@@ -247,7 +240,7 @@ public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_CONFIRM_CREDENTIALS_ACCOUNT) {
- openRegistrationActivity();
+ openRelayListActivity();
}
}
@@ -324,8 +317,8 @@ public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
}
}
- private void openRegistrationActivity() {
- Intent intent = new Intent(requireActivity(), EditTransportActivity.class);
+ private void openRelayListActivity() {
+ Intent intent = new Intent(requireActivity(), RelayListActivity.class);
startActivity(intent);
}
diff --git a/src/main/java/org/thoughtcrime/securesms/qr/QrActivity.java b/src/main/java/org/thoughtcrime/securesms/qr/QrActivity.java
index 540e6fe8f..4676930e8 100644
--- a/src/main/java/org/thoughtcrime/securesms/qr/QrActivity.java
+++ b/src/main/java/org/thoughtcrime/securesms/qr/QrActivity.java
@@ -44,6 +44,8 @@ import java.io.InputStream;
public class QrActivity extends BaseActionBarActivity implements View.OnClickListener {
private final static String TAG = QrActivity.class.getSimpleName();
+ public final static String EXTRA_SCAN_RELAY = "scan_relay";
+
private final static int REQUEST_CODE_IMAGE = 46243;
private final static int TAB_SHOW = 0;
private final static int TAB_SCAN = 1;
@@ -51,6 +53,7 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis
private TabLayout tabLayout;
private ViewPager viewPager;
private QrShowFragment qrShowFragment;
+ private boolean scanRelay;
@Override
protected void onPreCreate() {
@@ -61,8 +64,10 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
-
setContentView(R.layout.activity_qr);
+
+ scanRelay = getIntent().getBooleanExtra(EXTRA_SCAN_RELAY, false);
+
qrShowFragment = new QrShowFragment(this);
tabLayout = ViewUtil.findById(this, R.id.tab_layout);
viewPager = ViewUtil.findById(this, R.id.pager);
@@ -71,10 +76,11 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis
setSupportActionBar(ViewUtil.findById(this, R.id.toolbar));
assert getSupportActionBar() != null;
- getSupportActionBar().setTitle(R.string.menu_new_contact);
+ getSupportActionBar().setTitle(scanRelay? R.string.add_transport : R.string.menu_new_contact);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
- viewPager.setCurrentItem(TAB_SHOW);
+ viewPager.setCurrentItem(scanRelay? TAB_SCAN : TAB_SHOW);
+ if (scanRelay) tabLayout.setVisibility(View.GONE);
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
@@ -102,7 +108,13 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.perm_explain_access_to_camera_denied))
.onAllGranted(() -> ((QrScanFragment) adapter.getItem(TAB_SCAN)).handleQrScanWithPermissions(QrActivity.this))
- .onAnyDenied(() -> viewPager.setCurrentItem(TAB_SHOW))
+ .onAnyDenied(() -> {
+ if (scanRelay) {
+ finish();
+ } else {
+ viewPager.setCurrentItem(TAB_SHOW);
+ }
+ })
.execute();
}
}
@@ -149,7 +161,11 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis
if (permissions.length > 0
&& Manifest.permission.CAMERA.equals(permissions[0])
&& grantResults[0] == PackageManager.PERMISSION_DENIED) {
- viewPager.setCurrentItem(TAB_SHOW);
+ if (scanRelay) {
+ finish();
+ } else {
+ viewPager.setCurrentItem(TAB_SHOW);
+ }
// Workaround because sometimes something else requested the permissions before this class
// (probably the CameraView) and then this class didn't notice when it was denied
}
diff --git a/src/main/java/org/thoughtcrime/securesms/qr/QrCodeHandler.java b/src/main/java/org/thoughtcrime/securesms/qr/QrCodeHandler.java
index e2cf69561..e95d072eb 100644
--- a/src/main/java/org/thoughtcrime/securesms/qr/QrCodeHandler.java
+++ b/src/main/java/org/thoughtcrime/securesms/qr/QrCodeHandler.java
@@ -1,7 +1,9 @@
package org.thoughtcrime.securesms.qr;
import android.app.Activity;
+import android.content.DialogInterface;
import android.content.Intent;
+import android.util.Log;
import android.widget.Toast;
import androidx.annotation.StringRes;
@@ -17,15 +19,25 @@ import org.thoughtcrime.securesms.connect.AccountManager;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.util.IntentUtils;
import org.thoughtcrime.securesms.util.Util;
+import org.thoughtcrime.securesms.util.views.ProgressDialog;
+
+import chat.delta.rpc.Rpc;
+import chat.delta.rpc.RpcException;
public class QrCodeHandler {
+ private static final String TAG = QrCodeHandler.class.getSimpleName();
+
private final Activity activity;
private final DcContext dcContext;
+ private final Rpc rpc;
+ private final int accId;
public QrCodeHandler(Activity activity) {
this.activity = activity;
dcContext = DcHelper.getContext(activity);
+ rpc = DcHelper.getRpc(activity);
+ accId = dcContext.getAccountId();
}
public void onScanPerformed(IntentResult scanResult) {
@@ -67,10 +79,7 @@ public class QrCodeHandler {
case DcContext.DC_QR_ACCOUNT:
case DcContext.DC_QR_LOGIN:
final String scope = qrParsed.getText1();
- builder.setMessage(activity.getString(qrParsed.getState() == DcContext.DC_QR_ACCOUNT ? R.string.qraccount_ask_create_and_login_another : R.string.qrlogin_ask_login_another, scope));
- builder.setPositiveButton(R.string.ok, (dialog, which) -> {
- AccountManager.getInstance().addAccountFromQr(activity, rawString);
- });
+ setAddTransportDialog(activity, builder, rawString, scope);
builder.setNegativeButton(R.string.cancel, null);
builder.setCancelable(false);
break;
@@ -246,4 +255,43 @@ public class QrCodeHandler {
});
builder.setNegativeButton(android.R.string.cancel, null);
}
+
+ private void setAddTransportDialog(Activity activity, AlertDialog.Builder builder, String qrData, String transportName) {
+ builder.setTitle(R.string.confirm_add_transport);
+ builder.setMessage(transportName);
+ builder.setPositiveButton(R.string.ok, (d, w) -> {
+ ProgressDialog progressDialog = new ProgressDialog(activity);
+ progressDialog.setMessage(activity.getResources().getString(R.string.one_moment));
+ progressDialog.setCanceledOnTouchOutside(false);
+ progressDialog.setCancelable(false);
+ String cancel = activity.getResources().getString(android.R.string.cancel);
+ progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, cancel, (d2, w2) -> {
+ dcContext.stopOngoingProcess();
+ });
+ progressDialog.show();
+
+ Util.runOnAnyBackgroundThread(() -> {
+ String error = null;
+ try {
+ rpc.addTransportFromQr(accId, qrData);
+ } catch (RpcException e) {
+ Log.w(TAG, e);
+ error = e.getMessage();
+ }
+ final String finalError = error;
+ Util.runOnMain(() -> {
+ if (!progressDialog.isShowing()) return; // canceled dialog, nothing to do
+ if (finalError != null) {
+ Toast.makeText(activity, finalError, Toast.LENGTH_LONG).show();
+ }
+ try {
+ progressDialog.dismiss();
+ } catch (IllegalArgumentException e) {
+ // see https://stackoverflow.com/a/5102572/4557005
+ Log.w(TAG, e);
+ }
+ });
+ });
+ });
+ }
}
diff --git a/src/main/java/org/thoughtcrime/securesms/EditTransportActivity.java b/src/main/java/org/thoughtcrime/securesms/relay/EditRelayActivity.java
similarity index 94%
rename from src/main/java/org/thoughtcrime/securesms/EditTransportActivity.java
rename to src/main/java/org/thoughtcrime/securesms/relay/EditRelayActivity.java
index 9f7593887..62c5e905e 100644
--- a/src/main/java/org/thoughtcrime/securesms/EditTransportActivity.java
+++ b/src/main/java/org/thoughtcrime/securesms/relay/EditRelayActivity.java
@@ -1,4 +1,4 @@
-package org.thoughtcrime.securesms;
+package org.thoughtcrime.securesms.relay;
import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_PROXY_ENABLED;
import static org.thoughtcrime.securesms.connect.DcHelper.getContext;
@@ -10,6 +10,7 @@ import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
+import android.util.Log;
import android.util.Patterns;
import android.view.Menu;
import android.view.MenuInflater;
@@ -32,6 +33,11 @@ import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcProvider;
import com.google.android.material.textfield.TextInputEditText;
+import org.thoughtcrime.securesms.BaseActionBarActivity;
+import org.thoughtcrime.securesms.ConversationListActivity;
+import org.thoughtcrime.securesms.LogViewActivity;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.WelcomeActivity;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.permissions.Permissions;
@@ -49,7 +55,7 @@ import chat.delta.rpc.types.EnteredCertificateChecks;
import chat.delta.rpc.types.EnteredLoginParam;
import chat.delta.rpc.types.Socket;
-public class EditTransportActivity extends BaseActionBarActivity implements DcEventCenter.DcEventDelegate {
+public class EditRelayActivity extends BaseActionBarActivity implements DcEventCenter.DcEventDelegate {
private enum VerificationType {
EMAIL,
@@ -57,6 +63,9 @@ public class EditTransportActivity extends BaseActionBarActivity implements DcEv
PORT,
}
+ private static final String TAG = EditRelayActivity.class.getSimpleName();
+ public final static String EXTRA_ADDR = "extra_addr";
+
private TextInputEditText emailInput;
private TextInputEditText passwordInput;
@@ -118,11 +127,26 @@ public class EditTransportActivity extends BaseActionBarActivity implements DcEv
startActivity(new Intent(this, ProxySettingsActivity.class));
});
+ String addr = getIntent().getStringExtra(EXTRA_ADDR);
EnteredLoginParam config = null;
try {
List relays = rpc.listTransports(accId);
- if (!relays.isEmpty()) config = relays.get(0);
- } catch (RpcException ignored) {}
+ for (EnteredLoginParam relay : relays) {
+ if (addr != null && addr.equals(relay.addr)) {
+ config = relay;
+ break;
+ }
+ }
+ if (config == null && !relays.isEmpty()) {
+ Log.e(TAG, "Error got unknown address: " + addr);
+ finish();
+ return;
+ };
+ } catch (RpcException e) {
+ Log.e(TAG, "Error calling Rpc.listTransports()", e);
+ finish();
+ return;
+ }
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
diff --git a/src/main/java/org/thoughtcrime/securesms/relay/RelayListActivity.java b/src/main/java/org/thoughtcrime/securesms/relay/RelayListActivity.java
new file mode 100644
index 000000000..32bfb174d
--- /dev/null
+++ b/src/main/java/org/thoughtcrime/securesms/relay/RelayListActivity.java
@@ -0,0 +1,180 @@
+package org.thoughtcrime.securesms.relay;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.MenuItem;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AlertDialog;
+import androidx.recyclerview.widget.DividerItemDecoration;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.b44t.messenger.DcContext;
+import com.b44t.messenger.DcEvent;
+import com.google.zxing.integration.android.IntentIntegrator;
+import com.google.zxing.integration.android.IntentResult;
+
+import org.thoughtcrime.securesms.BaseActionBarActivity;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton;
+import org.thoughtcrime.securesms.connect.DcEventCenter;
+import org.thoughtcrime.securesms.connect.DcHelper;
+import org.thoughtcrime.securesms.qr.QrActivity;
+import org.thoughtcrime.securesms.qr.QrCodeHandler;
+import org.thoughtcrime.securesms.util.Util;
+import org.thoughtcrime.securesms.util.ViewUtil;
+
+import java.util.List;
+
+import chat.delta.rpc.Rpc;
+import chat.delta.rpc.RpcException;
+import chat.delta.rpc.types.EnteredLoginParam;
+
+public class RelayListActivity extends BaseActionBarActivity
+ implements RelayListAdapter.OnRelayClickListener, DcEventCenter.DcEventDelegate {
+
+ private static final String TAG = RelayListActivity.class.getSimpleName();
+
+ private RelayListAdapter adapter;
+ private Rpc rpc;
+ private int accId;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_relay_list);
+
+ rpc = DcHelper.getRpc(this);
+ accId = DcHelper.getContext(this).getAccountId();
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setTitle(R.string.transports);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ RecyclerView recyclerView = findViewById(R.id.relay_list);
+ PulsingFloatingActionButton fabAdd = findViewById(R.id.fab_add_relay);
+
+ // add padding to avoid content hidden behind system bars
+ ViewUtil.applyWindowInsets(recyclerView);
+ // Apply insets to prevent fab from being covered by system bars
+ ViewUtil.applyWindowInsetsAsMargin(fabAdd);
+
+ fabAdd.setOnClickListener(v -> {
+ new IntentIntegrator(this).setCaptureActivity(QrActivity.class).addExtra(QrActivity.EXTRA_SCAN_RELAY, true).initiateScan();
+ });
+
+ LinearLayoutManager layoutManager = new LinearLayoutManager(this);
+ // Add the default divider (uses the theme’s `android.R.attr.listDivider`)
+ DividerItemDecoration divider = new DividerItemDecoration(
+ recyclerView.getContext(),
+ layoutManager.getOrientation());
+ recyclerView.addItemDecoration(divider);
+ recyclerView.setLayoutManager(layoutManager);
+
+ adapter = new RelayListAdapter(this);
+ recyclerView.setAdapter(adapter);
+
+ loadRelays();
+
+ DcEventCenter eventCenter = DcHelper.getEventCenter(this);
+ eventCenter.addObserver(DcContext.DC_EVENT_CONFIGURE_PROGRESS, this);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ DcHelper.getEventCenter(this).removeObservers(this);
+ }
+
+ private void loadRelays() {
+ Util.runOnAnyBackgroundThread(() -> {
+ String mainRelayAddr = "";
+ try {
+ mainRelayAddr = rpc.getConfig(accId, DcHelper.CONFIG_CONFIGURED_ADDRESS);
+ } catch (RpcException e) {
+ Log.e(TAG, "RPC.getConfig() failed", e);
+ }
+ String finalMainRelayAddr = mainRelayAddr;
+
+ try {
+ List relays = rpc.listTransports(accId);
+
+ Util.runOnMain(() -> adapter.setRelays(relays, finalMainRelayAddr));
+ } catch (RpcException e) {
+ Log.e(TAG, "RPC.listTransports() failed", e);
+ Util.runOnMain(() -> adapter.setRelays(null, finalMainRelayAddr));
+ }
+ });
+ }
+
+ @Override
+ public void onRelayClick(EnteredLoginParam relay) {
+ if (relay.addr != null && !relay.addr.equals(adapter.getMainRelay())) {
+ Util.runOnAnyBackgroundThread(() -> {
+ try {
+ rpc.setConfig(accId, DcHelper.CONFIG_CONFIGURED_ADDRESS, relay.addr);
+ } catch (RpcException e) {
+ Log.e(TAG, "RPC.setConfig() failed", e);
+ }
+
+ loadRelays();
+ });
+ }
+ }
+
+ @Override
+ public void onRelayEdit(EnteredLoginParam relay) {
+ Intent intent = new Intent(this, EditRelayActivity.class);
+ intent.putExtra(EditRelayActivity.EXTRA_ADDR, relay.addr);
+ startActivity(intent);
+ }
+
+ @Override
+ public void onRelayDelete(EnteredLoginParam relay) {
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.remove_transport)
+ .setMessage(getString(R.string.confirm_remove_transport, relay.addr))
+ .setPositiveButton(R.string.ok, (dialog, which) -> {
+ try {
+ rpc.deleteTransport(accId, relay.addr);
+ loadRelays();
+ } catch (RpcException e) {
+ Log.e(TAG, "RPC.deleteTransport() failed", e);
+ }
+ })
+ .setNegativeButton(R.string.cancel, null)
+ .show();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ finish();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == IntentIntegrator.REQUEST_CODE) {
+ IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
+ QrCodeHandler qrCodeHandler = new QrCodeHandler(this);
+ qrCodeHandler.onScanPerformed(scanResult);
+ }
+ }
+
+ @Override
+ public void handleEvent(@NonNull DcEvent event) {
+ int eventId = event.getId();
+ if (eventId == DcContext.DC_EVENT_CONFIGURE_PROGRESS && event.getData1Int() == 1000) {
+ loadRelays();
+ }
+ }
+}
diff --git a/src/main/java/org/thoughtcrime/securesms/relay/RelayListAdapter.java b/src/main/java/org/thoughtcrime/securesms/relay/RelayListAdapter.java
new file mode 100644
index 000000000..365b9babf
--- /dev/null
+++ b/src/main/java/org/thoughtcrime/securesms/relay/RelayListAdapter.java
@@ -0,0 +1,104 @@
+package org.thoughtcrime.securesms.relay;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import org.thoughtcrime.securesms.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import chat.delta.rpc.types.EnteredLoginParam;
+
+public class RelayListAdapter extends RecyclerView.Adapter {
+
+ private List relays = new ArrayList<>();
+ private final OnRelayClickListener listener;
+ private String mainRelayAddr;
+
+ public interface OnRelayClickListener {
+ void onRelayClick(EnteredLoginParam relay);
+ void onRelayEdit(EnteredLoginParam relay);
+ void onRelayDelete(EnteredLoginParam relay);
+ }
+
+ public RelayListAdapter(OnRelayClickListener listener) {
+ this.listener = listener;
+ }
+
+ public String getMainRelay() {
+ return mainRelayAddr;
+ }
+
+ public void setRelays(@Nullable List relays, String mainRelayAddr) {
+ this.relays = relays != null ? relays : new ArrayList<>();
+ this.mainRelayAddr = mainRelayAddr;
+ notifyDataSetChanged();
+ }
+
+ @NonNull
+ @Override
+ public RelayViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.relay_list_item, parent, false);
+ return new RelayViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RelayViewHolder holder, int position) {
+ EnteredLoginParam relay = relays.get(position);
+ boolean isMain = relay.addr != null && relay.addr.equals(mainRelayAddr);
+ holder.bind(relay, isMain, listener);
+ }
+
+ @Override
+ public int getItemCount() {
+ return relays.size();
+ }
+
+ public static class RelayViewHolder extends RecyclerView.ViewHolder {
+ private final TextView titleText;
+ private final ImageView mainIndicator;
+ private final ImageView editButton;
+ private final ImageView deleteButton;
+
+ public RelayViewHolder(@NonNull View itemView) {
+ super(itemView);
+ titleText = itemView.findViewById(R.id.title);
+ mainIndicator = itemView.findViewById(R.id.main_indicator);
+ editButton = itemView.findViewById(R.id.edit_button);
+ deleteButton = itemView.findViewById(R.id.delete_button);
+ }
+
+ public void bind(EnteredLoginParam relay, boolean isMain, OnRelayClickListener listener) {
+ titleText.setText(relay.addr);
+ mainIndicator.setVisibility(isMain ? View.VISIBLE : View.GONE);
+ deleteButton.setVisibility(isMain ? View.GONE : View.VISIBLE);
+
+ itemView.setOnClickListener(v -> {
+ if (listener != null) {
+ listener.onRelayClick(relay);
+ }
+ });
+
+ editButton.setOnClickListener(v -> {
+ if (listener != null) {
+ listener.onRelayEdit(relay);
+ }
+ });
+
+ deleteButton.setOnClickListener(v -> {
+ if (listener != null) {
+ listener.onRelayDelete(relay);
+ }
+ });
+ }
+ }
+}
diff --git a/src/main/res/layout/activity_relay_list.xml b/src/main/res/layout/activity_relay_list.xml
new file mode 100644
index 000000000..243fc3ccb
--- /dev/null
+++ b/src/main/res/layout/activity_relay_list.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/res/layout/relay_list_item.xml b/src/main/res/layout/relay_list_item.xml
new file mode 100644
index 000000000..43e8a8a0b
--- /dev/null
+++ b/src/main/res/layout/relay_list_item.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/res/xml/preferences_advanced.xml b/src/main/res/xml/preferences_advanced.xml
index 9640bcb20..dc540cb4f 100644
--- a/src/main/res/xml/preferences_advanced.xml
+++ b/src/main/res/xml/preferences_advanced.xml
@@ -7,8 +7,8 @@
-
+