Compare commits

..

8 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 8217d3fc22 Replace AccessibilityDelegateCompat with ViewCompat.addAccessibilityAction for link actions
The previous AccessibilityDelegateCompat approach had two fatal flaws:
1. The delegate was only set when isTouchExplorationEnabled() returned
   true AT BIND TIME — a timing bug that caused TalkBack to see no
   actions if the view was bound before TalkBack fully initialised.
2. Even when set, the delegate/custom-action-ID approach did not
   reliably produce an "Actions" submenu in the TalkBack local context
   menu on Android 14+.

Replace the entire mechanism with ViewCompat.addAccessibilityAction(),
the modern API (androidx.core 1.5.0+) that TalkBack always surfaces
under "Actions". Actions are registered unconditionally at bind time;
stale actions from recycled views are removed via
ViewCompat.removeAccessibilityAction() before re-registration.

Delete the now-unused LinkAccessibilityDelegate class.

Agent-Logs-Url: https://github.com/ArcaneChat/android/sessions/95854760-c8e5-43d8-b7b4-55879a7c5dea

Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-05-01 11:22:20 +00:00
copilot-swe-agent[bot] dfa80abd13 Fix TalkBack custom actions: use AccessibilityDelegateCompat + ViewCompat
The previous code used View.AccessibilityDelegate (raw framework) and
AccessibilityNodeInfoCompat.wrap() which is deprecated and does not
reliably propagate addAction() to the real AccessibilityNodeInfo that
TalkBack reads.

Switch to AccessibilityDelegateCompat (AndroidX) set via
ViewCompat.setAccessibilityDelegate(). Its onInitializeAccessibilityNodeInfo
callback receives a properly-backed AccessibilityNodeInfoCompat so
info.addAction() reliably surfaces custom actions in the TalkBack
context menu (L-gesture) on all Android versions including API 36.

Also clear the delegate in the CALL/empty-text branch to prevent a
stale delegate remaining on a recycled view.

Agent-Logs-Url: https://github.com/ArcaneChat/android/sessions/87aa7735-a87e-473d-b80c-67e697cd4fa7

Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-05-01 11:02:12 +00:00
copilot-swe-agent[bot] 177b68a42c Fix: attach accessibility delegate to ConversationItem, not bodyText
bodyText has importantForAccessibility="no" in the XML layout, so
TalkBack focuses the parent ConversationItem. The delegate must be
set on `this` and must read spans from a bodyText reference, not
from the view it's attached to.

Agent-Logs-Url: https://github.com/ArcaneChat/android/sessions/598a695d-a5cd-4948-85e9-edce5e10c67c

Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-04-30 18:47:42 +00:00
copilot-swe-agent[bot] e8d3458a7b Add clarifying comments for accessibility implementation
- Explain why accessibility delegate is not set during batch selection
- Explain why map is cleared and rebuilt on each accessibility update
- Improves code maintainability and readability

Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-02-11 23:14:55 +00:00
copilot-swe-agent[bot] 54332e8bac Address code review feedback
- Reuse LinkAccessibilityDelegate instance instead of creating new one each time
- Add detailed comment explaining the magic number for action IDs
- Improves performance by avoiding unnecessary object allocations

Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-02-11 23:13:50 +00:00
copilot-swe-agent[bot] 6b06422fa1 Use internationalized string for accessibility link label
- Added accessibility_link_action string resource
- Updated LinkAccessibilityDelegate to accept Context and use resource
- Updated ConversationItem to pass context to delegate

Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-02-11 23:12:45 +00:00
copilot-swe-agent[bot] a7bd40f851 Add accessibility support for links in chat messages
- Created LinkAccessibilityDelegate to expose links as custom actions
- Updated ConversationItem to use delegate when TalkBack is enabled
- Each link becomes a separate accessible action with clear label

Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-02-11 23:11:36 +00:00
copilot-swe-agent[bot] 78659bb9e0 Initial plan 2026-02-11 23:05:37 +00:00
20 changed files with 115 additions and 574 deletions
-4
View File
@@ -1,9 +1,5 @@
# Delta Chat Android Changelog
## Unreleased
* Add context menu in long-pressing relays items instead of showing buttons
## v2.42.0
2026-02
@@ -33,6 +33,5 @@ public interface BindableConversationItem extends Unbindable {
void onShowFullClicked(DcMsg messageRecord);
void onDownloadClicked(DcMsg messageRecord);
void onReactionClicked(DcMsg messageRecord);
void onStickerClicked(DcMsg messageRecord);
}
}
@@ -1456,18 +1456,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
isEditing = false;
}
@Override
public void onStickerPicked(Uri stickerUri) {
if (isEditing) return;
sendSticker(stickerUri, "image/png");
}
public void refreshStickerPicker() {
if (emojiPicker != null) {
emojiPicker.refreshStickerPicker();
}
}
// media selected by the system keyboard
@Override
public void onMediaSelected(@NonNull Uri uri, String contentType) {
@@ -70,7 +70,6 @@ import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.ConversationAdaptiveActionsToolbar;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
@@ -402,7 +401,6 @@ public class ConversationFragment extends MessageSelectorFragment
boolean showReplyPrivately = chat.isMultiUser() && !messageRecord.isOutgoing() && canReply;
menu.findItem(R.id.menu_context_reply_privately).setVisible(showReplyPrivately);
menu.findItem(R.id.menu_add_to_home_screen).setVisible(messageRecord.getType() == DcMsg.DC_MSG_WEBXDC);
menu.findItem(R.id.menu_context_save_sticker).setVisible(false);
/*
boolean saved = messageRecord.getSavedMsgId() != 0;
@@ -556,46 +554,6 @@ public class ConversationFragment extends MessageSelectorFragment
}
}
private void handleSaveSticker(final DcMsg message) {
if (message.getType() != DcMsg.DC_MSG_STICKER) {
return;
}
File stickerFile = message.getFileAsFile();
if (stickerFile == null || !stickerFile.exists()) {
Toast.makeText(getContext(), R.string.error, Toast.LENGTH_SHORT).show();
return;
}
// Create stickers directory in internal storage
File stickersDir = new File(getContext().getFilesDir(), "stickers");
if (!stickersDir.exists()) {
stickersDir.mkdirs();
}
// Copy sticker to stickers directory
String fileName = System.currentTimeMillis() + "_" + stickerFile.getName();
File destFile = new File(stickersDir, fileName);
try (java.io.FileInputStream in = new java.io.FileInputStream(stickerFile);
java.io.FileOutputStream out = new java.io.FileOutputStream(destFile)) {
byte[] buffer = new byte[1024];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
Toast.makeText(getContext(), R.string.add_to_sticker_collection, Toast.LENGTH_SHORT).show();
// Refresh sticker picker to show the newly added sticker
if (getActivity() instanceof ConversationActivity) {
((ConversationActivity) getActivity()).refreshStickerPicker();
}
} catch (Exception e) {
Log.e(TAG, "Error saving sticker", e);
Toast.makeText(getContext(), R.string.error, Toast.LENGTH_SHORT).show();
}
}
@SuppressLint("RestrictedApi")
private void handleReplyMessage(final DcMsg message) {
if (getActivity() != null) {
@@ -989,18 +947,6 @@ public class ConversationFragment extends MessageSelectorFragment
ReactionsDetailsFragment dialog = ReactionsDetailsFragment.newInstance(messageRecord.getId());
dialog.show(getActivity().getSupportFragmentManager(), null);
}
@Override
public void onStickerClicked(DcMsg messageRecord) {
new androidx.appcompat.app.AlertDialog.Builder(getContext())
.setTitle(R.string.add_to_sticker_collection)
.setMessage(R.string.ask_add_sticker_to_collection)
.setPositiveButton(R.string.ok, (dialog, which) -> {
handleSaveSticker(messageRecord);
})
.setNegativeButton(R.string.cancel, null)
.show();
}
}
@Override
@@ -23,6 +23,7 @@ import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
@@ -36,6 +37,7 @@ import androidx.annotation.DimenRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.view.ViewCompat;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
@@ -66,6 +68,7 @@ import org.thoughtcrime.securesms.mms.VcardSlide;
import org.thoughtcrime.securesms.reactions.ReactionsConversationView;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Linkifier;
import org.thoughtcrime.securesms.util.LongClickCopySpan;
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
import org.thoughtcrime.securesms.util.MarkdownUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
@@ -73,6 +76,7 @@ import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.Stub;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@@ -125,6 +129,9 @@ public class ConversationItem extends BaseConversationItem
private Stub<VcardView> vcardViewStub;
private Stub<CallItemView> callViewStub;
private @Nullable EventListener eventListener;
// IDs of accessibility actions registered via ViewCompat.addAccessibilityAction, kept so they
// can be removed on rebind (RecyclerView reuses views for different messages).
private final List<Integer> linkActionIds = new ArrayList<>();
private int measureCalls;
@@ -413,6 +420,13 @@ public class ConversationItem extends BaseConversationItem
bodyText.setClickable(false);
bodyText.setFocusable(false);
// Remove any link actions registered for the previous message binding.
// RecyclerView reuses views, so stale actions from a previous item must be cleared.
for (int id : linkActionIds) {
ViewCompat.removeAccessibilityAction(this, id);
}
linkActionIds.clear();
String subject = messageRecord.getSubject();
String text = messageRecord.getText();
@@ -428,6 +442,31 @@ public class ConversationItem extends BaseConversationItem
}
bodyText.setText(spannable);
bodyText.setVisibility(View.VISIBLE);
// Register a TalkBack "Actions" entry for each link in the message.
// ViewCompat.addAccessibilityAction is used instead of an AccessibilityDelegateCompat because
// it is the modern, reliable API (available since androidx.core 1.5.0) that TalkBack always
// surfaces under the "Actions" submenu of the local context menu (L-gesture).
// The actions are registered unconditionally no isTouchExplorationEnabled() guard because
// the registration is cheap and the guard was a source of timing bugs at bind time.
// Links are only present when not in batch-selection mode (Linkifier is not called in that
// branch above), so no actions are added in batch-selection mode.
if (spannable instanceof Spanned) {
Spanned spanned = (Spanned) spannable;
final TextView tv = bodyText;
for (LongClickCopySpan span : spanned.getSpans(0, spanned.length(), LongClickCopySpan.class)) {
int start = spanned.getSpanStart(span);
int end = spanned.getSpanEnd(span);
if (start >= 0 && end > start && end <= spanned.length()) {
String linkText = spanned.subSequence(start, end).toString();
String label = context.getString(R.string.accessibility_link_action, linkText);
linkActionIds.add(ViewCompat.addAccessibilityAction(this, label, (v, args) -> {
span.onClick(tv);
return true;
}));
}
}
}
}
int downloadState = messageRecord.getDownloadState();
@@ -972,8 +1011,6 @@ public class ConversationItem extends BaseConversationItem
public void onClick(final View v, final Slide slide) {
if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) {
performClick();
} else if (eventListener != null) {
eventListener.onStickerClicked(messageRecord);
}
}
}
@@ -271,6 +271,12 @@ public class ConversationListItem extends RelativeLayout
} else {
deliveryStatusIndicator.setNone();
}
if (state == DcMsg.DC_STATE_OUT_FAILED) {
deliveryStatusIndicator.setTint(Color.RED);
} else {
deliveryStatusIndicator.resetTint();
}
}
int unreadCount = thread.getUnreadCount();
@@ -123,6 +123,12 @@ public class ConversationUpdateItem extends BaseConversationItem
else if (messageRecord.isPreparing()) deliveryStatusView.setPreparing();
else if (messageRecord.isPending()) deliveryStatusView.setPending();
else deliveryStatusView.setNone();
if (messageRecord.isFailed()) {
deliveryStatusView.setTint(Color.RED);
} else {
deliveryStatusView.setTint(textColor);
}
}
@Override
@@ -353,13 +353,6 @@ public class InputPanel extends ConstraintLayout
composeText.setSelection(start + emoji.length());
}
@Override
public void onStickerPicked(Uri stickerUri) {
if (listener != null) {
listener.onStickerPicked(stickerUri);
}
}
public interface Listener {
void onRecorderStarted();
void onRecorderLocked();
@@ -368,7 +361,6 @@ public class InputPanel extends ConstraintLayout
void onRecorderPermissionRequired();
void onEmojiToggle();
void onQuoteDismissed();
void onStickerPicked(Uri stickerUri);
}
private static class SlideToCancel {
@@ -1,13 +1,10 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -15,24 +12,15 @@ import androidx.core.util.Consumer;
import androidx.emoji2.emojipicker.EmojiPickerView;
import androidx.emoji2.emojipicker.EmojiViewItem;
import com.google.android.material.tabs.TabLayout;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.InputAwareLayout.InputView;
import org.thoughtcrime.securesms.util.ResUtil;
import java.io.File;
public class MediaKeyboard extends LinearLayout implements InputView, Consumer<EmojiViewItem>, StickerPickerView.StickerPickerListener {
public class MediaKeyboard extends FrameLayout implements InputView, Consumer<EmojiViewItem> {
private static final String TAG = MediaKeyboard.class.getSimpleName();
@Nullable private MediaKeyboardListener keyboardListener;
@Nullable private MediaKeyboardListener keyboardListener;
private EmojiPickerView emojiPicker;
private StickerPickerView stickerPicker;
private View stickerPickerContainer;
private View stickerPickerEmpty;
private TabLayout tabLayout;
public MediaKeyboard(@NonNull Context context) {
super(context);
@@ -63,90 +51,13 @@ public class MediaKeyboard extends LinearLayout implements InputView, Consumer<E
public void show() {
if (emojiPicker == null) {
setupViews();
emojiPicker = findViewById(R.id.emoji_picker);
emojiPicker.setOnEmojiPickedListener(this);
}
setVisibility(VISIBLE);
if (keyboardListener != null) keyboardListener.onShown();
}
private void setupViews() {
emojiPicker = findViewById(R.id.emoji_picker);
stickerPicker = findViewById(R.id.sticker_picker);
stickerPickerContainer = findViewById(R.id.sticker_picker_container);
stickerPickerEmpty = findViewById(R.id.sticker_picker_empty);
tabLayout = findViewById(R.id.media_keyboard_tabs);
if (emojiPicker != null) {
emojiPicker.setOnEmojiPickedListener(this);
}
if (stickerPicker != null) {
stickerPicker.setStickerPickerListener(this);
}
if (tabLayout != null) {
Drawable emojiIcon = ResUtil.getDrawable(getContext(), R.attr.conversation_emoji_toggle);
tabLayout.addTab(tabLayout.newTab().setIcon(emojiIcon));
tabLayout.addTab(tabLayout.newTab().setIcon(R.drawable.ic_sticker_24));
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
if (tab.getPosition() == 0) {
showEmojiPicker();
} else {
showStickerPicker();
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
}
}
private void showEmojiPicker() {
if (emojiPicker != null) {
emojiPicker.setVisibility(View.VISIBLE);
}
if (stickerPickerContainer != null) {
stickerPickerContainer.setVisibility(View.GONE);
}
}
private void showStickerPicker() {
if (emojiPicker != null) {
emojiPicker.setVisibility(View.GONE);
}
if (stickerPickerContainer != null) {
stickerPickerContainer.setVisibility(View.VISIBLE);
}
if (stickerPicker != null) {
stickerPicker.loadStickers();
updateStickerEmptyState();
}
}
private void updateStickerEmptyState() {
if (stickerPicker != null && stickerPickerEmpty != null) {
boolean hasStickers = stickerPicker.getAdapter() != null && stickerPicker.getAdapter().getItemCount() > 0;
stickerPickerEmpty.setVisibility(hasStickers ? View.GONE : View.VISIBLE);
}
}
public void refreshStickerPicker() {
if (stickerPicker != null) {
stickerPicker.loadStickers();
updateStickerEmptyState();
}
}
@Override
public void hide(boolean immediate) {
setVisibility(GONE);
@@ -159,24 +70,9 @@ public class MediaKeyboard extends LinearLayout implements InputView, Consumer<E
if (keyboardListener != null) keyboardListener.onEmojiPicked(emojiViewItem.getEmoji());
}
@Override
public void onStickerSelected(@NonNull File stickerFile) {
if (keyboardListener != null) {
keyboardListener.onStickerPicked(Uri.fromFile(stickerFile));
}
}
@Override
public void onStickerDeleted(@NonNull File stickerFile) {
// Update empty state after deletion
Log.i(TAG, "Sticker deleted: " + stickerFile.getName());
updateStickerEmptyState();
}
public interface MediaKeyboardListener {
void onShown();
void onHidden();
void onEmojiPicked(String emoji);
void onStickerPicked(Uri stickerUri);
}
}
@@ -1,187 +0,0 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.net.Uri;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class StickerPickerView extends RecyclerView {
private static final String TAG = StickerPickerView.class.getSimpleName();
private static final String STICKER_FOLDER = "stickers";
@Nullable private StickerPickerListener listener;
private GlideRequests glideRequests;
private File stickerDir;
public StickerPickerView(@NonNull Context context) {
super(context);
init(context);
}
public StickerPickerView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
glideRequests = GlideApp.with(context);
stickerDir = new File(context.getFilesDir(), STICKER_FOLDER);
setLayoutManager(new GridLayoutManager(context, 4));
loadStickers();
}
public void setStickerPickerListener(@Nullable StickerPickerListener listener) {
this.listener = listener;
}
public void loadStickers() {
List<File> stickerFiles = getSavedStickers();
setAdapter(new StickerAdapter(getContext(), glideRequests, stickerFiles, listener));
}
private List<File> getSavedStickers() {
List<File> stickerFiles = new ArrayList<>();
if (!stickerDir.exists()) {
return stickerFiles;
}
File[] files = stickerDir.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile()) {
stickerFiles.add(file);
}
}
}
// Sort stickers by filename
Collections.sort(stickerFiles, new Comparator<File>() {
@Override
public int compare(File f1, File f2) {
return f1.getName().compareToIgnoreCase(f2.getName());
}
});
return stickerFiles;
}
public interface StickerPickerListener {
void onStickerSelected(@NonNull File stickerFile);
void onStickerDeleted(@NonNull File stickerFile);
}
static class StickerAdapter extends RecyclerView.Adapter<StickerAdapter.StickerViewHolder> {
private final Context context;
private final GlideRequests glideRequests;
private final List<File> stickerFiles;
private final LayoutInflater layoutInflater;
private final StickerPickerListener listener;
StickerAdapter(@NonNull Context context,
@NonNull GlideRequests glideRequests,
@NonNull List<File> stickerFiles,
@Nullable StickerPickerListener listener) {
this.context = context;
this.glideRequests = glideRequests;
this.stickerFiles = stickerFiles;
this.layoutInflater = LayoutInflater.from(context);
this.listener = listener;
}
@Override
public @NonNull StickerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = layoutInflater.inflate(R.layout.sticker_picker_item, parent, false);
return new StickerViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull StickerViewHolder holder, int position) {
File stickerFile = stickerFiles.get(position);
holder.stickerFile = stickerFile;
glideRequests.load(stickerFile)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.into(holder.image);
}
@Override
public int getItemCount() {
return stickerFiles.size();
}
@Override
public void onViewRecycled(@NonNull StickerViewHolder holder) {
super.onViewRecycled(holder);
glideRequests.clear(holder.image);
}
private void deleteSticker(File stickerFile) {
if (stickerFile != null && stickerFile.exists()) {
new androidx.appcompat.app.AlertDialog.Builder(context)
.setTitle(R.string.delete)
.setMessage(R.string.ask_delete_sticker)
.setPositiveButton(R.string.delete, (dialog, which) -> {
if (stickerFile.delete()) {
int position = stickerFiles.indexOf(stickerFile);
if (position >= 0) {
stickerFiles.remove(position);
notifyItemRemoved(position);
}
if (listener != null) {
listener.onStickerDeleted(stickerFile);
}
}
})
.setNegativeButton(R.string.cancel, null)
.show();
}
}
class StickerViewHolder extends RecyclerView.ViewHolder {
private File stickerFile;
private final ImageView image;
StickerViewHolder(View itemView) {
super(itemView);
image = itemView.findViewById(R.id.sticker_image);
itemView.setOnClickListener(view -> {
if (listener != null && stickerFile != null) {
listener.onStickerSelected(stickerFile);
}
});
itemView.setOnLongClickListener(view -> {
if (stickerFile != null) {
deleteSticker(stickerFile);
return true;
}
return false;
});
}
}
}
}
@@ -3,9 +3,7 @@ package org.thoughtcrime.securesms.relay;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.ContextMenu;
import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
@@ -49,9 +47,6 @@ public class RelayListActivity extends BaseActionBarActivity
/** QR provided via Intent extras needs to be saved to pass it to QrCodeHandler when authorization finishes */
private String qrData = null;
/** Relay selected for context menu via onRelayLongClick() */
private EnteredLoginParam contextMenuRelay = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -148,54 +143,14 @@ public class RelayListActivity extends BaseActionBarActivity
}
@Override
public void onRelayLongClick(View view, EnteredLoginParam relay) {
contextMenuRelay = relay;
registerForContextMenu(view);
openContextMenu(view);
unregisterForContextMenu(view);
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
getMenuInflater().inflate(R.menu.relay_item_context, menu);
boolean nonNullAddr = contextMenuRelay != null && contextMenuRelay.addr != null;
boolean isMain = nonNullAddr && contextMenuRelay.addr.equals(adapter.getMainRelay());
menu.findItem(R.id.menu_delete_relay).setVisible(!isMain);
}
@Override
public void onContextMenuClosed(android.view.Menu menu) {
super.onContextMenuClosed(menu);
contextMenuRelay = null;
}
@Override
public boolean onContextItemSelected(@NonNull MenuItem item) {
if (contextMenuRelay == null) return super.onContextItemSelected(item);
int itemId = item.getItemId();
if (itemId == R.id.menu_edit_relay) {
onRelayEdit(contextMenuRelay);
contextMenuRelay = null;
return true;
} else if (itemId == R.id.menu_delete_relay) {
onRelayDelete(contextMenuRelay);
contextMenuRelay = null;
return true;
}
return super.onContextItemSelected(item);
}
private void onRelayEdit(EnteredLoginParam relay) {
public void onRelayEdit(EnteredLoginParam relay) {
Intent intent = new Intent(this, EditRelayActivity.class);
intent.putExtra(EditRelayActivity.EXTRA_ADDR, relay.addr);
startActivity(intent);
}
private void onRelayDelete(EnteredLoginParam relay) {
@Override
public void onRelayDelete(EnteredLoginParam relay) {
new AlertDialog.Builder(this)
.setTitle(R.string.remove_transport)
.setMessage(getString(R.string.confirm_remove_transport, relay.addr))
@@ -25,7 +25,8 @@ public class RelayListAdapter extends RecyclerView.Adapter<RelayListAdapter.Rela
public interface OnRelayClickListener {
void onRelayClick(EnteredLoginParam relay);
void onRelayLongClick(View view, EnteredLoginParam relay);
void onRelayEdit(EnteredLoginParam relay);
void onRelayDelete(EnteredLoginParam relay);
}
public RelayListAdapter(OnRelayClickListener listener) {
@@ -66,12 +67,16 @@ public class RelayListAdapter extends RecyclerView.Adapter<RelayListAdapter.Rela
private final TextView titleText;
private final TextView subtitleText;
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);
subtitleText = itemView.findViewById(R.id.subtitle);
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) {
@@ -79,6 +84,7 @@ public class RelayListAdapter extends RecyclerView.Adapter<RelayListAdapter.Rela
titleText.setText(parts.length == 2? parts[1] : parts[0]);
subtitleText.setText(parts.length == 2? parts[0] : "");
mainIndicator.setVisibility(isMain ? View.VISIBLE : View.INVISIBLE);
deleteButton.setVisibility(isMain ? View.GONE : View.VISIBLE);
itemView.setOnClickListener(v -> {
if (listener != null) {
@@ -86,11 +92,16 @@ public class RelayListAdapter extends RecyclerView.Adapter<RelayListAdapter.Rela
}
});
itemView.setOnLongClickListener(v -> {
editButton.setOnClickListener(v -> {
if (listener != null) {
listener.onRelayLongClick(v, relay);
listener.onRelayEdit(relay);
}
});
deleteButton.setOnClickListener(v -> {
if (listener != null) {
listener.onRelayDelete(relay);
}
return true;
});
}
}
-5
View File
@@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M460,600Q529,600 580,555Q631,510 640,442L320,532Q346,564 382,582Q418,600 460,600ZM294,450L400,420Q404,392 386,371Q368,350 340,350Q315,350 297.5,367.5Q280,385 280,410Q280,421 284,431Q288,441 294,450ZM534,380L640,350Q645,322 626.5,301Q608,280 580,280Q555,280 537.5,297.5Q520,315 520,340Q520,351 524,361Q528,371 534,380ZM640,840L200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,640L640,840ZM600,760L600,680Q600,647 623.5,623.5Q647,600 680,600L760,600L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L600,760ZM600,760L600,760L600,760Q600,760 600,760Q600,760 600,760ZM200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200L200,200Q200,200 200,200Q200,200 200,200L200,600L200,600Q200,600 200,623.5Q200,647 200,680L200,760Z"/>
</vector>
@@ -1,69 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.emoji.MediaKeyboard
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/emoji_drawer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<FrameLayout
android:id="@+id/media_keyboard_container"
<androidx.emoji2.emojipicker.EmojiPickerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/emoji_picker"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="?emoji_drawer_background">
<androidx.emoji2.emojipicker.EmojiPickerView
android:id="@+id/emoji_picker"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?emoji_drawer_background"
app:emojiGridColumns="12"/>
<FrameLayout
android:id="@+id/sticker_picker_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<org.thoughtcrime.securesms.components.emoji.StickerPickerView
android:id="@+id/sticker_picker"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?emoji_drawer_background"
android:paddingTop="4dp"
android:clipToPadding="false"
android:clipChildren="false"/>
<TextView
android:id="@+id/sticker_picker_empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/sticker_picker_empty_hint"
android:textColor="?android:attr/textColorSecondary"
android:textSize="16sp"
android:visibility="gone"/>
</FrameLayout>
</FrameLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/media_keyboard_tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:background="?emoji_drawer_background"
app:tabMode="fixed"
app:tabGravity="center"
app:tabIndicatorGravity="top"
app:tabIconTint="?attr/colorControlNormal"
app:tabPaddingStart="16dp"
app:tabPaddingEnd="16dp"
app:tabPaddingTop="10dp"
app:tabPaddingBottom="10dp"
/>
app:emojiGridColumns="12"/>
</org.thoughtcrime.securesms.components.emoji.MediaKeyboard>
@@ -2,69 +2,18 @@
<!-- WARNING: on modifications also update the layout-land folder. -->
<org.thoughtcrime.securesms.components.emoji.MediaKeyboard
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/emoji_drawer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<FrameLayout
android:id="@+id/media_keyboard_container"
<androidx.emoji2.emojipicker.EmojiPickerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/emoji_picker"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="?emoji_drawer_background">
<androidx.emoji2.emojipicker.EmojiPickerView
android:id="@+id/emoji_picker"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?emoji_drawer_background"
app:emojiGridColumns="9"/>
<FrameLayout
android:id="@+id/sticker_picker_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<org.thoughtcrime.securesms.components.emoji.StickerPickerView
android:id="@+id/sticker_picker"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?emoji_drawer_background"
android:paddingTop="4dp"
android:clipToPadding="false"
android:clipChildren="false"/>
<TextView
android:id="@+id/sticker_picker_empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/sticker_picker_empty_hint"
android:textColor="?android:attr/textColorSecondary"
android:textSize="16sp"
android:visibility="gone"/>
</FrameLayout>
</FrameLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/media_keyboard_tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:background="?emoji_drawer_background"
app:tabMode="fixed"
app:tabGravity="center"
app:tabIndicatorGravity="top"
app:tabIconTint="?attr/colorControlNormal"
app:tabPaddingStart="16dp"
app:tabPaddingEnd="16dp"
app:tabPaddingTop="10dp"
app:tabPaddingBottom="10dp"
/>
app:emojiGridColumns="9"/>
</org.thoughtcrime.securesms.components.emoji.MediaKeyboard>
+26
View File
@@ -59,4 +59,30 @@
</LinearLayout>
<ImageView
android:id="@+id/delete_button"
android:layout_width="48dp"
android:layout_height="72dp"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:clickable="true"
android:focusable="true"
android:contentDescription="@string/remove_transport"
android:background="@drawable/touch_highlight_background"
android:src="@drawable/ic_delete_white_24dp"
app:tint="?attr/conversation_list_item_date_color" />
<ImageView
android:id="@+id/edit_button"
android:layout_width="48dp"
android:layout_height="72dp"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:clickable="true"
android:focusable="true"
android:contentDescription="@string/edit_transport"
android:background="@drawable/touch_highlight_background"
android:src="@drawable/ic_create_white_24dp"
app:tint="?attr/conversation_list_item_date_color" />
</LinearLayout>
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ImageView android:id="@+id/sticker_image"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="100dp"
android:padding="8dp"
android:scaleType="centerInside"/>
@@ -40,11 +40,6 @@
android:visible="false"
app:showAsAction="never" />
<item android:title="@string/add_to_sticker_collection"
android:id="@+id/menu_context_save_sticker"
android:visible="false"
app:showAsAction="never" />
<item android:title="@string/info"
android:id="@+id/menu_context_details"
app:showAsAction="never" />
-11
View File
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="@string/edit_transport"
android:id="@+id/menu_edit_relay"/>
<item android:title="@string/remove_transport"
android:id="@+id/menu_delete_relay"/>
</menu>
+3 -3
View File
@@ -178,9 +178,6 @@
<string name="add_to_sticker_collection">Add to Sticker Collection</string>
<string name="add_stickers_instructions">To add stickers, tap "Open Sticker Folder", create a subfolder for your sticker pack, and drag image and sticker files there</string>
<string name="open_sticker_folder">Open Sticker Folder</string>
<string name="ask_add_sticker_to_collection">Add this sticker to your collection?</string>
<string name="ask_delete_sticker">Delete this sticker?</string>
<string name="sticker_picker_empty_hint">Saved stickers will appear here.</string>
<string name="images">Images</string>
<string name="audio">Audio</string>
<string name="voice_message">Voice Message</string>
@@ -1182,6 +1179,9 @@
<string name="perm_enable_bg_reminder_title">Tap here to receive messages while Delta Chat is in the background.</string>
<string name="perm_enable_bg_already_done">You already allowed Delta Chat to receive messages in the background.\n\nIf messages still do not arrive in background, please also check your system settings.</string>
<!-- Accessibility -->
<string name="accessibility_link_action">Open link: %1$s</string>
<!-- device messages for updates -->
<string name="update_2_0">What\'s new?\n\n💯 End-to-end encryption is reliable and forever now. Padlocks 🔒 are gone!\n\n✉️ Classic email without end-to-end encryption is marked with a letter symbol\n\n😻 New enhanced profile screen for all your contacts\n\n🔲 New button for quick access to apps used in a chat\n\n❤️ Please donate to help us remain independent and continue to bring improvements: %1$s</string>
</resources>