1
0
Fork 1

add option to restrict avatar access model to contacts

This commit is contained in:
Daniel Gultsch 2024-12-23 17:24:03 +01:00 committed by Arne
parent a555141178
commit ee33f3f6c9
5 changed files with 185 additions and 127 deletions

View file

@ -4577,23 +4577,32 @@ public class XmppConnectionService extends Service {
}).start(); }).start();
} }
public void publishAvatar(final Account account, final Uri image, final OnAvatarPublication callback) { public void publishAvatarAsync(
new Thread(() -> { final Account account,
final Bitmap.CompressFormat format = Config.AVATAR_FORMAT; final Uri image,
final int size = Config.AVATAR_SIZE; final boolean open,
final Avatar avatar = getFileBackend().getPepAvatar(image, size, format); final OnAvatarPublication callback) {
if (avatar != null) { new Thread(() -> publishAvatar(account, image, open, callback)).start();
if (!getFileBackend().save(avatar)) { }
Log.d(Config.LOGTAG, "unable to save vcard");
callback.onAvatarPublicationFailed(R.string.error_saving_avatar);
return;
}
publishAvatar(account, avatar, callback);
} else {
callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting);
}
}).start();
private void publishAvatar(
final Account account,
final Uri image,
final boolean open,
final OnAvatarPublication callback) {
final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
final int size = Config.AVATAR_SIZE;
final Avatar avatar = getFileBackend().getPepAvatar(image, size, format);
if (avatar != null) {
if (!getFileBackend().save(avatar)) {
Log.d(Config.LOGTAG, "unable to save vcard");
callback.onAvatarPublicationFailed(R.string.error_saving_avatar);
return;
}
publishAvatar(account, avatar, open, callback);
} else {
callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting);
}
} }
private void publishMucAvatar(Conversation conversation, Avatar avatar, OnAvatarPublication callback) { private void publishMucAvatar(Conversation conversation, Avatar avatar, OnAvatarPublication callback) {
@ -4631,10 +4640,14 @@ public class XmppConnectionService extends Service {
}); });
} }
public void publishAvatar(Account account, final Avatar avatar, final OnAvatarPublication callback) { public void publishAvatar(
final Account account,
final Avatar avatar,
final boolean open,
final OnAvatarPublication callback) {
final Bundle options; final Bundle options;
if (account.getXmppConnection().getFeatures().pepPublishOptions()) { if (account.getXmppConnection().getFeatures().pepPublishOptions()) {
options = PublishOptions.openAccess(); options = open ? PublishOptions.openAccess() : PublishOptions.presenceAccess();
} else { } else {
options = null; options = null;
} }
@ -4706,7 +4719,7 @@ public class XmppConnectionService extends Service {
}); });
} }
public void republishAvatarIfNeeded(Account account) { public void republishAvatarIfNeeded(final Account account) {
if (account.getAxolotlService().isPepBroken()) { if (account.getAxolotlService().isPepBroken()) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping republication of avatar because pep is broken"); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping republication of avatar because pep is broken");
return; return;
@ -4735,12 +4748,19 @@ public class XmppConnectionService extends Service {
@Override @Override
public void accept(final Iq packet) { public void accept(final Iq packet) {
if (packet.getType() == Iq.Type.RESULT || errorIsItemNotFound(packet)) { if (packet.getType() == Iq.Type.RESULT || errorIsItemNotFound(packet)) {
Avatar serverAvatar = parseAvatar(packet); final Avatar serverAvatar = parseAvatar(packet);
if (serverAvatar == null && account.getAvatar() != null) { if (serverAvatar == null && account.getAvatar() != null) {
Avatar avatar = fileBackend.getStoredPepAvatar(account.getAvatar()); final Avatar avatar =
fileBackend.getStoredPepAvatar(account.getAvatar());
if (avatar != null) { if (avatar != null) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": avatar on server was null. republishing"); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": avatar on server was null. republishing");
publishAvatar(account, fileBackend.getStoredPepAvatar(account.getAvatar()), null); // publishing as 'open' - old server (that requires
// republication) likely doesn't support access models anyway
publishAvatar(
account,
fileBackend.getStoredPepAvatar(account.getAvatar()),
true,
null);
} else { } else {
Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": error rereading avatar"); Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": error rereading avatar");
} }

View file

@ -14,19 +14,12 @@ import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.View.OnLongClickListener; import android.view.View.OnLongClickListener;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.databinding.DataBindingUtil; import androidx.databinding.DataBindingUtil;
import com.canhub.cropper.CropImage; import com.canhub.cropper.CropImage;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.concurrent.atomic.AtomicBoolean;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityPublishProfilePictureBinding; import eu.siacs.conversations.databinding.ActivityPublishProfilePictureBinding;
@ -35,89 +28,97 @@ import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.interfaces.OnAvatarPublication; import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
import eu.siacs.conversations.utils.PhoneHelper; import eu.siacs.conversations.utils.PhoneHelper;
import java.util.concurrent.atomic.AtomicBoolean;
public class PublishProfilePictureActivity extends XmppActivity implements XmppConnectionService.OnAccountUpdate, OnAvatarPublication { public class PublishProfilePictureActivity extends XmppActivity
implements XmppConnectionService.OnAccountUpdate, OnAvatarPublication {
public static final int REQUEST_CHOOSE_PICTURE = 0x1337; public static final int REQUEST_CHOOSE_PICTURE = 0x1337;
private ImageView avatar; private ActivityPublishProfilePictureBinding binding;
private TextView hintOrWarning;
private TextView secondaryHint;
private Button cancelButton;
private Button publishButton;
private Uri avatarUri; private Uri avatarUri;
private Uri defaultUri; private Uri defaultUri;
private Account account; private Account account;
private boolean support = false; private boolean support = false;
private boolean publishing = false; private boolean publishing = false;
private final AtomicBoolean handledExternalUri = new AtomicBoolean(false); private final AtomicBoolean handledExternalUri = new AtomicBoolean(false);
private final OnLongClickListener backToDefaultListener = new OnLongClickListener() { private final OnLongClickListener backToDefaultListener =
new OnLongClickListener() {
@Override @Override
public boolean onLongClick(View v) { public boolean onLongClick(View v) {
avatarUri = defaultUri; avatarUri = defaultUri;
loadImageIntoPreview(defaultUri); loadImageIntoPreview(defaultUri);
return true; return true;
} }
}; };
private boolean mInitialAccountSetup; private boolean mInitialAccountSetup;
@Override @Override
public void onAvatarPublicationSucceeded() { public void onAvatarPublicationSucceeded() {
runOnUiThread(() -> { runOnUiThread(
if (mInitialAccountSetup) { () -> {
Intent intent = new Intent(getApplicationContext(), StartConversationActivity.class); if (mInitialAccountSetup) {
StartConversationActivity.addInviteUri(intent, getIntent()); Intent intent =
intent.putExtra("init", true); new Intent(
intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); getApplicationContext(), StartConversationActivity.class);
startActivity(intent); StartConversationActivity.addInviteUri(intent, getIntent());
} intent.putExtra("init", true);
Toast.makeText(PublishProfilePictureActivity.this, intent.putExtra(
R.string.avatar_has_been_published, EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString());
Toast.LENGTH_SHORT).show(); startActivity(intent);
finish(); }
}); Toast.makeText(
PublishProfilePictureActivity.this,
R.string.avatar_has_been_published,
Toast.LENGTH_SHORT)
.show();
finish();
});
} }
@Override @Override
public void onAvatarPublicationFailed(int res) { public void onAvatarPublicationFailed(final int res) {
runOnUiThread(() -> { runOnUiThread(
hintOrWarning.setText(res); () -> {
hintOrWarning.setVisibility(View.VISIBLE); this.binding.hintOrWarning.setText(res);
publishing = false; this.binding.hintOrWarning.setVisibility(View.VISIBLE);
togglePublishButton(true, R.string.publish); this.publishing = false;
}); togglePublishButton(true, R.string.publish);
});
} }
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
ActivityPublishProfilePictureBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_publish_profile_picture); this.binding =
DataBindingUtil.setContentView(this, R.layout.activity_publish_profile_picture);
setSupportActionBar(binding.toolbar); setSupportActionBar(binding.toolbar);
Activities.setStatusAndNavigationBarColors(this, binding.getRoot()); Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
this.avatar = findViewById(R.id.account_image); this.binding.publishButton.setOnClickListener(
this.cancelButton = findViewById(R.id.cancel_button); v -> {
this.publishButton = findViewById(R.id.publish_button); final boolean open = !this.binding.contactOnly.isChecked();
this.hintOrWarning = findViewById(R.id.hint_or_warning); final var uri = this.avatarUri;
this.secondaryHint = findViewById(R.id.secondary_hint); if (uri == null) {
this.publishButton.setOnClickListener(v -> { return;
if (avatarUri != null) { }
publishing = true; publishing = true;
togglePublishButton(false, R.string.publishing); togglePublishButton(false, R.string.publishing);
xmppConnectionService.publishAvatar(account, avatarUri, this); xmppConnectionService.publishAvatarAsync(account, uri, open, this);
} });
}); this.binding.cancelButton.setOnClickListener(
this.cancelButton.setOnClickListener(
v -> { v -> {
if (mInitialAccountSetup) { if (mInitialAccountSetup) {
final Intent intent = final Intent intent =
new Intent( new Intent(
getApplicationContext(), StartConversationActivity.class); getApplicationContext(), StartConversationActivity.class);
intent.putExtra("init", true); if (xmppConnectionService != null
&& xmppConnectionService.getAccounts().size() == 1) {
intent.putExtra("init", true);
}
StartConversationActivity.addInviteUri(intent, getIntent()); StartConversationActivity.addInviteUri(intent, getIntent());
if (account != null) { if (account != null) {
intent.putExtra( intent.putExtra(
@ -127,11 +128,12 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC
} }
finish(); finish();
}); });
this.avatar.setOnClickListener(v -> chooseAvatar(this)); this.binding.accountImage.setOnClickListener(v -> chooseAvatar(this));
this.defaultUri = PhoneHelper.getProfilePictureUri(getApplicationContext()); this.defaultUri = PhoneHelper.getProfilePictureUri(getApplicationContext());
if (savedInstanceState != null) { if (savedInstanceState != null) {
this.avatarUri = savedInstanceState.getParcelable("uri"); this.avatarUri = savedInstanceState.getParcelable("uri");
this.handledExternalUri.set(savedInstanceState.getBoolean("handle_external_uri",false)); this.handledExternalUri.set(
savedInstanceState.getBoolean("handle_external_uri", false));
} }
} }
@ -144,8 +146,8 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC
@Override @Override
public boolean onOptionsItemSelected(final MenuItem item) { public boolean onOptionsItemSelected(final MenuItem item) {
if (item.getItemId() == R.id.action_delete_avatar) { if (item.getItemId() == R.id.action_delete_avatar) {
if (xmppConnectionService != null && account != null) { if (account != null) {
xmppConnectionService.deleteAvatar(account); deleteAvatar(account);
} }
return true; return true;
} else { } else {
@ -153,6 +155,22 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC
} }
} }
private void deleteAvatar(final Account account) {
new MaterialAlertDialogBuilder(this)
.setTitle(R.string.delete_avatar)
.setMessage(R.string.delete_avatar_message)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(
R.string.confirm,
(d, v) -> {
if (xmppConnectionService != null) {
xmppConnectionService.deleteAvatar(account);
}
})
.create()
.show();
}
@Override @Override
public void onSaveInstanceState(@NonNull Bundle outState) { public void onSaveInstanceState(@NonNull Bundle outState) {
if (this.avatarUri != null) { if (this.avatarUri != null) {
@ -191,9 +209,9 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC
final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*"); intent.setType("image/*");
activity.startActivityForResult( activity.startActivityForResult(
Intent.createChooser(intent, activity.getString(R.string.attach_choose_picture)), Intent.createChooser(
REQUEST_CHOOSE_PICTURE intent, activity.getString(R.string.attach_choose_picture)),
); REQUEST_CHOOSE_PICTURE);
} else { } else {
CropImage.activity() CropImage.activity()
.setOutputCompressFormat(Bitmap.CompressFormat.PNG) .setOutputCompressFormat(Bitmap.CompressFormat.PNG)
@ -212,7 +230,9 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC
} }
private void reloadAvatar() { private void reloadAvatar() {
this.support = this.account.getXmppConnection() != null && this.account.getXmppConnection().getFeatures().pep(); this.support =
this.account.getXmppConnection() != null
&& this.account.getXmppConnection().getFeatures().pep();
if (this.avatarUri == null) { if (this.avatarUri == null) {
if (this.account.getAvatar() != null || this.defaultUri == null) { if (this.account.getAvatar() != null || this.defaultUri == null) {
loadImageIntoPreview(null); loadImageIntoPreview(null);
@ -233,21 +253,22 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC
final Uri uri = intent != null ? intent.getData() : null; final Uri uri = intent != null ? intent.getData() : null;
if (uri != null && handledExternalUri.compareAndSet(false,true)) { if (uri != null && handledExternalUri.compareAndSet(false, true)) {
cropUri(uri); cropUri(uri);
return; return;
} }
if (this.mInitialAccountSetup) { if (this.mInitialAccountSetup) {
this.cancelButton.setText(R.string.skip); this.binding.cancelButton.setText(R.string.skip);
} }
configureActionBar(getSupportActionBar(), !this.mInitialAccountSetup && !handledExternalUri.get()); configureActionBar(
getSupportActionBar(), !this.mInitialAccountSetup && !handledExternalUri.get());
} }
public void cropUri(final Uri uri) { public void cropUri(final Uri uri) {
if (Build.VERSION.SDK_INT >= 28) { if (Build.VERSION.SDK_INT >= 28) {
loadImageIntoPreview(uri); loadImageIntoPreview(uri);
if (this.avatar.getDrawable() instanceof AnimatedImageDrawable || this.avatar.getDrawable() instanceof FileBackend.SVGDrawable) { if (this.binding.accountImage.getDrawable() instanceof AnimatedImageDrawable || this.binding.accountImage.getDrawable() instanceof FileBackend.SVGDrawable) {
this.avatarUri = uri; this.avatarUri = uri;
return; return;
} }
@ -259,7 +280,7 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC
.start(this); .start(this);
} }
protected void loadImageIntoPreview(Uri uri) { protected void loadImageIntoPreview(final Uri uri) {
Drawable bm = null; Drawable bm = null;
if (uri == null) { if (uri == null) {
@ -267,46 +288,46 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC
} else { } else {
try { try {
bm = xmppConnectionService.getFileBackend().cropCenterSquareDrawable(uri, (int) getResources().getDimension(R.dimen.publish_avatar_size)); bm = xmppConnectionService.getFileBackend().cropCenterSquareDrawable(uri, (int) getResources().getDimension(R.dimen.publish_avatar_size));
} catch (Exception e) { } catch (final Exception e) {
Log.d(Config.LOGTAG, "unable to load bitmap into image view", e); Log.d(Config.LOGTAG, "unable to load bitmap into image view", e);
} }
} }
if (bm == null) { if (bm == null) {
togglePublishButton(false, R.string.publish); togglePublishButton(false, R.string.publish);
this.hintOrWarning.setVisibility(View.VISIBLE); this.binding.hintOrWarning.setVisibility(View.VISIBLE);
this.hintOrWarning.setText(R.string.error_publish_avatar_converting); this.binding.hintOrWarning.setText(R.string.error_publish_avatar_converting);
return; return;
} }
this.avatar.setImageDrawable(bm); this.binding.accountImage.setImageDrawable(bm);
if (Build.VERSION.SDK_INT >= 28 && bm instanceof AnimatedImageDrawable) { if (Build.VERSION.SDK_INT >= 28 && bm instanceof AnimatedImageDrawable) {
((AnimatedImageDrawable) bm).start(); ((AnimatedImageDrawable) bm).start();
} }
if (support) { if (support) {
togglePublishButton(uri != null, R.string.publish); togglePublishButton(uri != null, R.string.publish);
this.hintOrWarning.setVisibility(View.INVISIBLE); this.binding.hintOrWarning.setVisibility(View.INVISIBLE);
} else { } else {
togglePublishButton(false, R.string.publish); togglePublishButton(false, R.string.publish);
this.hintOrWarning.setVisibility(View.VISIBLE); this.binding.hintOrWarning.setVisibility(View.VISIBLE);
if (account.getStatus() == Account.State.ONLINE) { if (account.getStatus() == Account.State.ONLINE) {
this.hintOrWarning.setText(R.string.error_publish_avatar_no_server_support); this.binding.hintOrWarning.setText(R.string.error_publish_avatar_no_server_support);
} else { } else {
this.hintOrWarning.setText(R.string.error_publish_avatar_offline); this.binding.hintOrWarning.setText(R.string.error_publish_avatar_offline);
} }
} }
if (this.defaultUri == null || this.defaultUri.equals(uri)) { if (this.defaultUri == null || this.defaultUri.equals(uri)) {
this.secondaryHint.setVisibility(View.INVISIBLE); this.binding.secondaryHint.setVisibility(View.INVISIBLE);
this.avatar.setOnLongClickListener(null); this.binding.accountImage.setOnLongClickListener(null);
} else if (this.defaultUri != null) { } else if (this.defaultUri != null) {
this.secondaryHint.setVisibility(View.VISIBLE); this.binding.secondaryHint.setVisibility(View.VISIBLE);
this.avatar.setOnLongClickListener(this.backToDefaultListener); this.binding.accountImage.setOnLongClickListener(this.backToDefaultListener);
} }
} }
protected void togglePublishButton(boolean enabled, @StringRes int res) { protected void togglePublishButton(boolean enabled, @StringRes int res) {
final boolean status = enabled && !publishing; final boolean status = enabled && !publishing;
this.publishButton.setText(publishing ? R.string.publishing : res); this.binding.publishButton.setText(publishing ? R.string.publishing : res);
this.publishButton.setEnabled(status); this.binding.publishButton.setEnabled(status);
} }
public void refreshUiReal() { public void refreshUiReal() {

View file

@ -1,16 +1,13 @@
package eu.siacs.conversations.xmpp.pep; package eu.siacs.conversations.xmpp.pep;
import android.os.Bundle; import android.os.Bundle;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Namespace;
import im.conversations.android.xmpp.model.stanza.Iq; import im.conversations.android.xmpp.model.stanza.Iq;
public class PublishOptions { public class PublishOptions {
private PublishOptions() { private PublishOptions() {}
}
public static Bundle openAccess() { public static Bundle openAccess() {
final Bundle options = new Bundle(); final Bundle options = new Bundle();
@ -18,6 +15,12 @@ public class PublishOptions {
return options; return options;
} }
public static Bundle presenceAccess() {
final Bundle options = new Bundle();
options.putString("pubsub#access_model", "presence");
return options;
}
public static Bundle persistentWhitelistAccess() { public static Bundle persistentWhitelistAccess() {
final Bundle options = new Bundle(); final Bundle options = new Bundle();
options.putString("pubsub#persist_items", "true"); options.putString("pubsub#persist_items", "true");
@ -32,14 +35,15 @@ public class PublishOptions {
options.putString("pubsub#send_last_published_item", "never"); options.putString("pubsub#send_last_published_item", "never");
options.putString("pubsub#max_items", "max"); options.putString("pubsub#max_items", "max");
options.putString("pubsub#notify_delete", "true"); options.putString("pubsub#notify_delete", "true");
options.putString("pubsub#notify_retract", "true"); //one could also set notify=true on the retract options.putString(
"pubsub#notify_retract", "true"); // one could also set notify=true on the retract
return options; return options;
} }
public static boolean preconditionNotMet(Iq response) { public static boolean preconditionNotMet(Iq response) {
final Element error = response.getType() == Iq.Type.ERROR ? response.findChild("error") : null; final Element error =
response.getType() == Iq.Type.ERROR ? response.findChild("error") : null;
return error != null && error.hasChild("precondition-not-met", Namespace.PUBSUB_ERROR); return error != null && error.hasChild("precondition-not-met", Namespace.PUBSUB_ERROR);
} }
} }

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"> <layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<RelativeLayout <RelativeLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -19,18 +20,19 @@
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<LinearLayout <ScrollView
android:layout_width="fill_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/app_bar_layout" android:layout_above="@+id/button_bar"
android:layout_marginLeft="@dimen/activity_horizontal_margin" android:layout_below="@id/app_bar_layout">
android:layout_marginTop="@dimen/activity_vertical_margin"
android:layout_marginRight="@dimen/activity_horizontal_margin"
android:layout_marginBottom="@dimen/activity_vertical_margin">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/activity_horizontal_margin"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:layout_marginRight="@dimen/activity_horizontal_margin"
android:layout_marginBottom="@dimen/activity_vertical_margin"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:orientation="vertical"> android:orientation="vertical">
@ -62,17 +64,26 @@
android:text="@string/or_long_press_for_default" android:text="@string/or_long_press_for_default"
android:textAppearance="?textAppearanceBodyMedium" /> android:textAppearance="?textAppearanceBodyMedium" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/contact_only"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="12dp"
android:text="@string/show_to_contacts_only" />
<TextView <TextView
android:id="@+id/hint_or_warning" android:id="@+id/hint_or_warning"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginVertical="12dp"
android:layout_marginBottom="8dp"
android:textAppearance="?textAppearanceBodyMedium" android:textAppearance="?textAppearanceBodyMedium"
android:textColor="?colorError" /> android:textColor="?colorError"
tools:text="@string/error_saving_avatar" />
</LinearLayout> </LinearLayout>
</LinearLayout>
</ScrollView>
<RelativeLayout <RelativeLayout
android:id="@+id/button_bar" android:id="@+id/button_bar"

View file

@ -1408,4 +1408,6 @@
<string name="pref_prefer_ipv6">Prefer IPv6</string> <string name="pref_prefer_ipv6">Prefer IPv6</string>
<string name="pref_use_colored_muc_names">Use colored user names</string> <string name="pref_use_colored_muc_names">Use colored user names</string>
<string name="pref_use_colored_muc_names_summary">Use colored user names in group chats</string> <string name="pref_use_colored_muc_names_summary">Use colored user names in group chats</string>
<string name="delete_avatar_message">Would you like to delete your avatar? Some clients might continue to display a cached copy of your avatar.</string>
<string name="show_to_contacts_only">Show to contacts only</string>
</resources> </resources>