Compare commits

..

12 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] d8ba2652ce Show contact avatar in @mention suggestion list items
Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-02-21 18:16:17 +00:00
copilot-swe-agent[bot] d0d1a9c902 Add @mentions autocomplete when drafting a message in group chats
Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-02-21 18:03:49 +00:00
copilot-swe-agent[bot] f39be17803 Initial plan 2026-02-21 17:50:07 +00:00
adb b99426b7c6 Merge pull request #118 from ArcaneChat/copilot/setup-copilot-instructions-again
Enhance Copilot instructions with build timing, CI/CD details, and troubleshooting
2026-02-19 05:33:38 +01:00
adbenitez de1eedc63f update .github/copilot-instructions.md 2026-02-19 05:32:04 +01:00
copilot-swe-agent[bot] ec4db3e58f Enhance Copilot instructions with detailed build, test, and CI/CD information
Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-02-18 20:52:39 +00:00
copilot-swe-agent[bot] 0d2175b641 Initial plan 2026-02-18 20:49:42 +00:00
adb 97620b8a3c Merge pull request #114 from ArcaneChat/copilot/improve-attachment-selector
Convert attachment selector to horizontal scrollable row with uniform cell sizing
2026-02-18 04:40:13 +01:00
adbenitez 24317b38bf tweak src/main/res/layout/attachment_type_selector.xml 2026-02-18 04:35:01 +01:00
copilot-swe-agent[bot] 6b28ed15e3 Fix cell sizing: uniform width with ellipsized text
Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-02-18 03:02:49 +00:00
copilot-swe-agent[bot] b8b203e517 Convert attachment selector from fixed grid to scrollable row
Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-02-18 02:47:04 +00:00
copilot-swe-agent[bot] e5c1e477f0 Initial plan 2026-02-18 02:44:54 +00:00
13 changed files with 632 additions and 283 deletions
+148 -9
View File
@@ -8,7 +8,7 @@ ArcaneChat is a Delta Chat Android client built on top of the official Delta Cha
- **Language:** Java (Java 8 compatibility)
- **Build System:** Gradle with Android Gradle Plugin 8.11.1
- **Min SDK:** 21 (Android 5.0)
- **Target SDK:** 35 (Android 15)
- **Target SDK:** 36 (Android 16)
- **NDK Version:** 27.0.12077973
- **Native Components:** Rust (deltachat-core-rust submodule)
- **UI Framework:** Android SDK, Material Design Components
@@ -17,13 +17,23 @@ ArcaneChat is a Delta Chat Android client built on top of the official Delta Cha
## Repository Structure
- `src/main/` - Main application source code
- `src/main/java/org/thoughtcrime/securesms/` - Main UI components
- `src/main/java/com/b44t/messenger/` - Delta Chat core integration
- `src/main/java/chat/delta/rpc/` - JSON-RPC bindings (generated, don't edit manually)
- `src/main/res/` - Android resources (layouts, strings, drawables)
- `src/androidTest/` - Instrumented tests (UI tests, benchmarks)
- `src/androidTest/java/com/b44t/messenger/uitests/` - UI tests
- `src/androidTest/java/com/b44t/messenger/uibenchmarks/` - Performance benchmarks
- `src/gplay/` - Google Play flavor-specific code
- `src/foss/` - F-Droid/FOSS flavor-specific code
- `jni/deltachat-core-rust/` - Native Rust core library (submodule)
- `jni/deltachat-core-rust/` - Native Rust core library (submodule, **don't edit directly**)
- `scripts/` - Build and helper scripts
- `scripts/ndk-make.sh` - Build native libraries
- `scripts/install-toolchains.sh` - Install Rust cross-compilation toolchains
- `scripts/generate-rpc-bindings.sh` - Generate JSON-RPC bindings
- `docs/` - Documentation
- `fastlane/` - App store metadata and screenshots
- `.github/workflows/` - CI/CD workflows (GitHub Actions)
## Build Instructions
@@ -33,17 +43,36 @@ ArcaneChat is a Delta Chat Android client built on top of the official Delta Cha
```bash
git submodule update --init --recursive
```
This MUST be done first before any build attempts.
2. **Build native libraries:**
2. **Set up environment variables:**
```bash
export ANDROID_NDK_ROOT=/path/to/ndk/27.0.12077973
export PATH=${PATH}:${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/:${ANDROID_NDK_ROOT}
```
Note: Path format varies by OS (linux-x86_64, darwin-x86_64, etc.)
3. **Install Rust toolchains:**
```bash
scripts/install-toolchains.sh
```
Required for building the native Rust components.
4. **Build native libraries:**
```bash
scripts/ndk-make.sh
```
Note: First run may take significant time as it builds for all architectures (armeabi-v7a, arm64-v8a, x86, x86_64)
**IMPORTANT:** First run takes 30-60 minutes as it builds for all architectures (armeabi-v7a, arm64-v8a, x86, x86_64).
For faster development builds, build for a single architecture:
```bash
scripts/ndk-make.sh armeabi-v7a
```
3. **Build APK:**
5. **Build APK:**
```bash
./gradlew assembleDebug
```
Build time: ~2-5 minutes after native libraries are built.
### Build Flavors
@@ -55,6 +84,24 @@ ArcaneChat is a Delta Chat Android client built on top of the official Delta Cha
- Debug APKs: `build/outputs/apk/gplay/debug/` and `build/outputs/apk/fat/debug/`
- Release APKs require signing configuration in `~/.gradle/gradle.properties`
### Common Build Issues
1. **Missing NDK or incorrect version:**
- Error: `ANDROID_NDK_ROOT not set` or native library missing
- Solution: Install NDK 27.0.12077973 and set ANDROID_NDK_ROOT environment variable
2. **Submodules not initialized:**
- Error: Missing deltachat-core-rust files
- Solution: Run `git submodule update --init --recursive`
3. **Gradle wrapper validation:**
- Always validate gradle wrapper before building: `./gradlew wrapper --gradle-version=current`
- Wrapper is validated in CI via `gradle/actions/wrapper-validation@v4`
4. **Clean build issues:**
- If build fails, try: `./gradlew clean && scripts/ndk-make.sh && ./gradlew assembleDebug`
- Remove `build/` directory if clean doesn't work
## Testing
### Running Unit Tests
@@ -62,16 +109,19 @@ ArcaneChat is a Delta Chat Android client built on top of the official Delta Cha
```bash
./gradlew test
```
Expected duration: 1-3 minutes
### Running Instrumented Tests
1. **Disable animations** on your device/emulator:
- Developer Options → Set "Window animation scale", "Transition animation scale", and "Animator duration scale" to 0x
- **CRITICAL:** Tests will fail if animations are enabled
2. **Run tests:**
```bash
./gradlew connectedAndroidTest
```
Expected duration: 10-30 minutes depending on device/emulator
### Online Tests
@@ -129,6 +179,14 @@ TEST_MAIL_PW=yourpassword
- Java bindings are in `src/main/java/com/b44t/messenger/Dc*.java`
- JSON-RPC bindings in `chat.delta.rpc.*` package (generated via dcrpcgen)
### Generating JSON-RPC Bindings
To regenerate JSON-RPC bindings after core changes:
```bash
./scripts/generate-rpc-bindings.sh
```
**Note:** Requires Rust tooling and [dcrpcgen tool](https://github.com/chatmail/dcrpcgen) installed
### Working with Translations
- Translations managed via Transifex (not in repository)
@@ -142,6 +200,43 @@ Decode crash symbols:
$ANDROID_NDK_ROOT/ndk-stack --sym obj/local/armeabi-v7a --dump crash.txt > decoded.txt
```
## Validation and Quality Checks
### Pre-commit Checks
Before committing changes, always run:
1. **Gradle wrapper validation:**
```bash
./gradlew wrapper --gradle-version=current
```
2. **Build verification:**
```bash
./gradlew assembleDebug
```
3. **Unit tests:**
```bash
./gradlew test
```
4. **Code style:** Match existing code style in modified files (no automatic formatter configured)
### When to Rebuild Native Libraries
Rebuild native libraries (`scripts/ndk-make.sh`) when:
- Updating deltachat-core-rust submodule
- Modifying anything in `jni/` directory
- Changing NDK version
- After `git clean -fdx` or fresh clone
**DO NOT** rebuild native libraries for:
- Pure Java/Kotlin code changes
- Resource file changes
- Gradle configuration changes (unless changing native library linking)
- Documentation updates
## WebXDC Support
ArcaneChat has extended WebXDC support:
@@ -152,12 +247,24 @@ ArcaneChat has extended WebXDC support:
## Important Files
- `build.gradle` - Main build configuration
- `build.gradle` - Main build configuration (Android Gradle Plugin 8.11.1, Java 8 compatibility)
- `CONTRIBUTING.md` - Contribution guidelines
- `BUILDING.md` - Detailed build setup
- `BUILDING.md` - Detailed build setup instructions
- `RELEASE.md` - Release process
- `proguard-rules.pro` - ProGuard configuration
- `google-services.json` - Firebase configuration (gplay flavor)
- `proguard-rules.pro` - ProGuard configuration (enabled for both debug and release)
- `google-services.json` - Firebase configuration (gplay flavor only)
- `settings.gradle` - Gradle settings
- `.github/workflows/` - CI/CD configuration
## Dependencies and Constraints
- **Java Version:** Java 8 compatibility (do not use Java 9+ features)
- **Gradle:** Use wrapper (`./gradlew`) to ensure correct Gradle version
- **NDK:** Must use version 27.0.12077973 (specified in build.gradle)
- **Min SDK:** 21 (Android 5.0) - code must be compatible
- **Target SDK:** 36 (Android 16) - test on this API level when possible
- **ProGuard:** Always enabled - ensure ProGuard rules are correct for new dependencies
- **Multi-dex:** Enabled - app exceeds 65k method limit
## Package Structure
@@ -173,3 +280,35 @@ ArcaneChat has extended WebXDC support:
- Native library must be rebuilt after core changes
- ProGuard is enabled in both debug and release builds
- Multi-dex is enabled due to app size
## CI/CD Workflows
### Preview APK Workflow (.github/workflows/preview-apk.yml)
Runs on every pull request to build and upload a preview APK:
1. **Setup steps:**
- Checks out repository with submodules
- Validates Fastlane metadata
- Sets up Rust cache (working-directory: jni/deltachat-core-rust)
- Sets up Java 17 (Temurin distribution)
- Sets up Android SDK
- Caches Gradle dependencies
- Sets up NDK r27
2. **Build process:**
```bash
scripts/install-toolchains.sh && scripts/ndk-make.sh armeabi-v7a
./gradlew --no-daemon -PABI_FILTER=armeabi-v7a assembleFossDebug
```
Note: Builds only armeabi-v7a for faster CI builds
3. **Output:** Uploads APK artifact to GitHub Actions
### Important CI Considerations
- Always validate Gradle wrapper before committing changes
- Fastlane metadata must be valid (validated in CI)
- Use `--no-daemon` flag for Gradle in CI environments
- CI builds use FOSS flavor to avoid Google Services dependencies
- Expected CI build time: 15-25 minutes for full workflow
@@ -32,8 +32,6 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
@@ -93,7 +91,6 @@ public class ContactSelectionListFragment extends Fragment
private TextView emptyView;
private ActionMode actionMode;
private ActionMode.Callback actionModeCallback;
private ActivityResultLauncher<Intent> newContactLauncher;
@Override
public void onActivityCreated(Bundle icicle) {
@@ -101,22 +98,6 @@ public class ContactSelectionListFragment extends Fragment
dcContext = DcHelper.getContext(getActivity());
DcHelper.getEventCenter(getActivity()).addObserver(DcContext.DC_EVENT_CONTACTS_CHANGED, this);
// Register activity result launcher
newContactLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
int contactId = result.getData().getIntExtra(NewContactActivity.CONTACT_ID_EXTRA, 0);
if (contactId != 0) {
selectedContacts.add(contactId);
deselectedContacts.remove(contactId);
}
getLoaderManager().restartLoader(0, null, ContactSelectionListFragment.this);
}
}
);
initializeCursor();
}
@@ -333,7 +314,7 @@ public class ContactSelectionListFragment extends Fragment
intent.putExtra(NewContactActivity.ADDR_EXTRA, cursorFilter);
}
if (isMulti()) {
newContactLauncher.launch(intent);
startActivityForResult(intent, CONTACT_ADDR_RESULT_CODE);
} else {
requireContext().startActivity(intent);
}
@@ -381,4 +362,17 @@ public class ContactSelectionListFragment extends Fragment
getLoaderManager().restartLoader(0, null, ContactSelectionListFragment.this);
}
}
@Override
public void onActivityResult(int reqCode, int resultCode, final Intent data) {
super.onActivityResult(reqCode, resultCode, data);
if (resultCode == Activity.RESULT_OK && reqCode == CONTACT_ADDR_RESULT_CODE) {
int contactId = data.getIntExtra(NewContactActivity.CONTACT_ID_EXTRA, 0);
if (contactId != 0) {
selectedContacts.add(contactId);
deselectedContacts.remove(contactId);
}
getLoaderManager().restartLoader(0, null, ContactSelectionListFragment.this);
}
}
}
@@ -72,6 +72,8 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.media3.session.MediaController;
import androidx.media3.session.SessionCommand;
import androidx.media3.session.SessionToken;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
@@ -90,6 +92,7 @@ import org.thoughtcrime.securesms.components.HidingLinearLayout;
import org.thoughtcrime.securesms.components.InputAwareLayout;
import org.thoughtcrime.securesms.components.InputPanel;
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener;
import org.thoughtcrime.securesms.components.MentionAdapter;
import org.thoughtcrime.securesms.components.ScaleStableImageView;
import org.thoughtcrime.securesms.components.SendButton;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
@@ -208,6 +211,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private boolean isEditing = false;
private boolean switchedProfile = false;
private RecyclerView mentionSuggestions;
private MentionAdapter mentionAdapter;
@Override
protected void onCreate(Bundle state, boolean ready) {
this.context = ApplicationContext.getInstance(getApplicationContext());
@@ -956,6 +962,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
inputPanel = ViewUtil.findById(this, R.id.bottom_panel);
backgroundView = ViewUtil.findById(this, R.id.conversation_background);
messageRequestBottomView = ViewUtil.findById(this, R.id.conversation_activity_message_request_bottom_bar);
mentionSuggestions = ViewUtil.findById(this, R.id.mention_suggestions);
ImageButton quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle);
@@ -1057,6 +1064,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
recipient = new Recipient(this, dcChat);
glideRequests = GlideApp.with(this);
mentionAdapter = new MentionAdapter(glideRequests);
mentionSuggestions.setLayoutManager(new LinearLayoutManager(this));
mentionSuggestions.setAdapter(mentionAdapter);
mentionAdapter.setOnMentionClickListener(contact -> insertMention(contact));
setComposePanelVisibility(true);
initializeContactRequest();
}
@@ -1560,6 +1572,77 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
private void updateMentionSuggestions() {
if (!dcChat.isMultiUser()) {
mentionSuggestions.setVisibility(View.GONE);
return;
}
Editable text = composeText.getText();
if (text == null) {
mentionSuggestions.setVisibility(View.GONE);
return;
}
int cursorPos = composeText.getSelectionStart();
if (cursorPos <= 0) {
mentionSuggestions.setVisibility(View.GONE);
return;
}
String textBeforeCursor = text.toString().substring(0, cursorPos);
int atIndex = textBeforeCursor.lastIndexOf('@');
if (atIndex < 0) {
mentionSuggestions.setVisibility(View.GONE);
return;
}
// Check that there's no space between "@" and the cursor
String query = textBeforeCursor.substring(atIndex + 1);
if (query.contains(" ") || query.contains("\n")) {
mentionSuggestions.setVisibility(View.GONE);
return;
}
DcContext dcContext = DcHelper.getContext(context);
int[] contactIds = dcContext.getChatContacts(chatId);
List<DcContact> matched = new ArrayList<>();
String lowerQuery = query.toLowerCase();
for (int id : contactIds) {
if (id <= DcContact.DC_CONTACT_ID_LAST_SPECIAL) continue;
DcContact contact = dcContext.getContact(id);
String displayName = contact.getDisplayName().toLowerCase();
String addr = contact.getAddr().toLowerCase();
if (displayName.contains(lowerQuery) || addr.contains(lowerQuery)) {
matched.add(contact);
}
}
if (matched.isEmpty()) {
mentionSuggestions.setVisibility(View.GONE);
} else {
mentionAdapter.setContacts(matched);
mentionSuggestions.setVisibility(View.VISIBLE);
}
}
private void insertMention(DcContact contact) {
Editable text = composeText.getText();
if (text == null) return;
int cursorPos = composeText.getSelectionStart();
if (cursorPos <= 0) return;
String textBeforeCursor = text.toString().substring(0, cursorPos);
int atIndex = textBeforeCursor.lastIndexOf('@');
if (atIndex < 0) return;
String mention = "@" + contact.getDisplayName() + " ";
text.replace(atIndex, cursorPos, mention);
composeText.setSelection(atIndex + mention.length());
mentionSuggestions.setVisibility(View.GONE);
}
private class ComposeKeyPressedListener implements OnKeyListener, OnClickListener, TextWatcher, OnFocusChangeListener {
int beforeLength;
@@ -1593,6 +1676,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (composeText.getTextTrimmed().length() == 0 || beforeLength == 0) {
composeText.postDelayed(ConversationActivity.this::updateToggleButtonState, 50);
}
updateMentionSuggestions();
}
@Override
@@ -87,6 +87,7 @@ public class ConversationFragment extends MessageSelectorFragment
private static final String TAG = ConversationFragment.class.getSimpleName();
private static final int SCROLL_ANIMATION_THRESHOLD = 50;
private static final int CODE_ADD_EDIT_CONTACT = 77;
private final ActionModeCallback actionModeCallback = new ActionModeCallback();
private final ItemClickListener selectionClickListener = new ConversationFragmentItemClickListener();
@@ -948,6 +949,17 @@ public class ConversationFragment extends MessageSelectorFragment
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CODE_ADD_EDIT_CONTACT && getContext() != null) {
// ApplicationContext.getInstance(getContext().getApplicationContext())
// .getJobManager()
// .add(new DirectoryRefreshJob(getContext().getApplicationContext(), false));
}
}
private class ActionModeCallback implements ActionMode.Callback {
@Override
@@ -11,8 +11,6 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
@@ -50,7 +48,6 @@ public class ProfileFragment extends Fragment
private ActionMode actionMode;
private final ActionModeCallback actionModeCallback = new ActionModeCallback();
private ActivityResultLauncher<Intent> pickContactLauncher;
private DcContext dcContext;
protected int chatId;
@@ -63,41 +60,6 @@ public class ProfileFragment extends Fragment
chatId = getArguments() != null ? getArguments().getInt(CHAT_ID_EXTRA, -1) : -1;
contactId = getArguments().getInt(CONTACT_ID_EXTRA, -1);
dcContext = DcHelper.getContext(requireContext());
// Register activity result launcher
pickContactLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
Intent data = result.getData();
List<Integer> selected = data.getIntegerArrayListExtra(ContactMultiSelectionActivity.CONTACTS_EXTRA);
List<Integer> deselected = data.getIntegerArrayListExtra(ContactMultiSelectionActivity.DESELECTED_CONTACTS_EXTRA);
Util.runOnAnyBackgroundThread(() -> {
if (deselected != null) {
// Remove members that were deselected
int[] members = dcContext.getChatContacts(chatId);
for (int contactId : deselected) {
for (int memberId : members) {
if (memberId == contactId) {
dcContext.removeContactFromChat(chatId, memberId);
break;
}
}
}
}
if (selected != null) {
// Add new members
for (Integer contactId : selected) {
if (contactId != null) {
dcContext.addContactToChat(chatId, contactId);
}
}
}
});
}
}
);
}
@Override
@@ -248,7 +210,7 @@ public class ProfileFragment extends Fragment
preselectedContacts.add(memberId);
}
intent.putExtra(ContactSelectionListFragment.PRESELECTED_CONTACTS, preselectedContacts);
pickContactLauncher.launch(intent);
startActivityForResult(intent, REQUEST_CODE_PICK_CONTACT);
}
public void onQrInvite() {
@@ -342,4 +304,36 @@ public class ProfileFragment extends Fragment
adapter.clearSelection();
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode==REQUEST_CODE_PICK_CONTACT && resultCode==Activity.RESULT_OK && data!=null) {
List<Integer> selected = data.getIntegerArrayListExtra(ContactMultiSelectionActivity.CONTACTS_EXTRA);
List<Integer> deselected = data.getIntegerArrayListExtra(ContactMultiSelectionActivity.DESELECTED_CONTACTS_EXTRA);
Util.runOnAnyBackgroundThread(() -> {
if (deselected != null) {
// Remove members that were deselected
int[] members = dcContext.getChatContacts(chatId);
for (int contactId : deselected) {
for (int memberId : members) {
if (memberId == contactId) {
dcContext.removeContactFromChat(chatId, memberId);
break;
}
}
}
}
if (selected != null) {
// Add new members
for (Integer contactId : selected) {
if (contactId != null) {
dcContext.addContactToChat(chatId, contactId);
}
}
}
});
}
}
}
@@ -0,0 +1,94 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.b44t.messenger.DcContact;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import java.util.ArrayList;
import java.util.List;
public class MentionAdapter extends RecyclerView.Adapter<MentionAdapter.ViewHolder> {
public interface OnMentionClickListener {
void onMentionClicked(DcContact contact);
}
private final List<DcContact> contacts = new ArrayList<>();
private final GlideRequests glideRequests;
private OnMentionClickListener listener;
public MentionAdapter(@NonNull GlideRequests glideRequests) {
this.glideRequests = glideRequests;
}
public void setContacts(List<DcContact> newContacts) {
contacts.clear();
contacts.addAll(newContacts);
notifyDataSetChanged();
}
public void setOnMentionClickListener(OnMentionClickListener listener) {
this.listener = listener;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.mention_list_item, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
DcContact contact = contacts.get(position);
Context context = holder.itemView.getContext();
holder.displayName.setText(contact.getDisplayName());
String addr = contact.getAddr();
String name = contact.getName();
if (!name.isEmpty() && !name.equals(addr)) {
holder.address.setText(addr);
holder.address.setVisibility(View.VISIBLE);
} else {
holder.address.setVisibility(View.GONE);
}
holder.avatar.setAvatar(glideRequests, new Recipient(context, contact), false);
holder.itemView.setOnClickListener(v -> {
if (listener != null) {
listener.onMentionClicked(contact);
}
});
}
@Override
public int getItemCount() {
return contacts.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
final AvatarImageView avatar;
final TextView displayName;
final TextView address;
ViewHolder(@NonNull View itemView) {
super(itemView);
avatar = itemView.findViewById(R.id.mention_avatar);
displayName = itemView.findViewById(R.id.mention_display_name);
address = itemView.findViewById(R.id.mention_address);
}
}
}
@@ -18,8 +18,6 @@ import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
@@ -57,22 +55,11 @@ public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
CheckBoxPreference multiDeviceCheckbox;
CheckBoxPreference mvboxMoveCheckbox;
CheckBoxPreference onlyFetchMvboxCheckbox;
private ActivityResultLauncher<Intent> screenLockLauncher;
@Override
public void onCreate(Bundle paramBundle) {
super.onCreate(paramBundle);
// Register activity result launcher for screen lock
screenLockLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == RESULT_OK) {
openRelayListActivity();
}
}
);
showEmails = (ListPreference) this.findPreference("pref_show_emails");
if (showEmails != null) {
showEmails.setOnPreferenceChangeListener((preference, newValue) -> {
@@ -171,7 +158,8 @@ public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
Preference relayListBtn = this.findPreference("pref_relay_list_button");
if (relayListBtn != null) {
relayListBtn.setOnPreferenceClickListener(((preference) -> {
if (!applyScreenLockWithLauncher()) {
boolean result = ScreenLockUtil.applyScreenLock(requireActivity(), getString(R.string.transports), getString(R.string.enter_system_secret_to_continue), REQUEST_CODE_CONFIRM_CREDENTIALS_ACCOUNT);
if (!result) {
openRelayListActivity();
}
return true;
@@ -203,17 +191,12 @@ public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
onlyFetchMvboxCheckbox.setChecked(0!=dcContext.getConfigInt(CONFIG_ONLY_FETCH_MVBOX));
}
private boolean applyScreenLockWithLauncher() {
android.app.KeyguardManager keyguardManager = (android.app.KeyguardManager) requireActivity().getSystemService(Context.KEYGUARD_SERVICE);
Intent intent;
if (keyguardManager != null) {
intent = keyguardManager.createConfirmDeviceCredentialIntent(getString(R.string.transports), getString(R.string.enter_system_secret_to_continue));
if (intent != null) {
screenLockLauncher.launch(intent);
return true;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_CONFIRM_CREDENTIALS_ACCOUNT) {
openRelayListActivity();
}
return false;
}
protected File copyToCacheDir(Uri uri) throws IOException {
@@ -10,8 +10,6 @@ import android.view.View;
import android.widget.CheckBox;
import android.widget.TextView;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
@@ -32,22 +30,11 @@ import org.thoughtcrime.securesms.util.Util;
public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
private ListPreference mediaQuality;
private ListPreference autoDownload;
private ActivityResultLauncher<Intent> screenLockLauncher;
@Override
public void onCreate(Bundle paramBundle) {
super.onCreate(paramBundle);
// Register activity result launcher for screen lock
screenLockLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == RESULT_OK) {
performBackup();
}
}
);
mediaQuality = (ListPreference) this.findPreference("pref_compression");
if (mediaQuality != null) {
mediaQuality.setOnPreferenceChangeListener((preference, newValue) -> {
@@ -126,6 +113,14 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_CONFIRM_CREDENTIALS_BACKUP) {
performBackup();
}
}
public static CharSequence getSummary(Context context) {
final String quality;
if (Prefs.isHardCompressionEnabled(context)) {
@@ -136,19 +131,6 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
return context.getString(R.string.pref_outgoing_media_quality) + " " + quality;
}
private boolean applyScreenLockWithLauncher() {
android.app.KeyguardManager keyguardManager = (android.app.KeyguardManager) requireActivity().getSystemService(Context.KEYGUARD_SERVICE);
Intent intent;
if (keyguardManager != null) {
intent = keyguardManager.createConfirmDeviceCredentialIntent(getString(R.string.pref_backup), getString(R.string.enter_system_secret_to_continue));
if (intent != null) {
screenLockLauncher.launch(intent);
return true;
}
}
return false;
}
/***********************************************************************************************
* Backup
**********************************************************************************************/
@@ -156,7 +138,8 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
private class BackupListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(@NonNull Preference preference) {
if (!applyScreenLockWithLauncher()) {
boolean result = ScreenLockUtil.applyScreenLock(requireActivity(), getString(R.string.pref_backup), getString(R.string.enter_system_secret_to_continue), REQUEST_CODE_CONFIRM_CREDENTIALS_BACKUP);
if (!result) {
performBackup();
}
return true;
@@ -16,8 +16,6 @@ import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationManagerCompat;
@@ -42,30 +40,11 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
private CheckBoxPreference notificationsEnabled;
private CheckBoxPreference mentionNotifEnabled;
private CheckBoxPreference reliableService;
private ActivityResultLauncher<Intent> ringtonePickerLauncher;
@Override
public void onCreate(Bundle paramBundle) {
super.onCreate(paramBundle);
// Register activity result launcher for ringtone picker
ringtonePickerLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == RESULT_OK && result.getData() != null) {
Uri uri = result.getData().getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
if (Settings.System.DEFAULT_NOTIFICATION_URI.equals(uri)) {
Prefs.removeNotificationRingtone(getContext());
} else {
Prefs.setNotificationRingtone(getContext(), uri != null ? uri : Uri.EMPTY);
}
initializeRingtoneSummary(findPreference(Prefs.RINGTONE_PREF));
}
}
);
this.findPreference(Prefs.LED_COLOR_PREF)
.setOnPreferenceChangeListener(new ListSummaryListener());
this.findPreference(Prefs.RINGTONE_PREF)
@@ -87,7 +66,7 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, Settings.System.DEFAULT_NOTIFICATION_URI);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current);
ringtonePickerLauncher.launch(intent);
startActivityForResult(intent, REQUEST_CODE_NOTIFICATION_SELECTED);
return true;
});
@@ -158,6 +137,22 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
reliableService.setOnPreferenceChangeListener(this);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_NOTIFICATION_SELECTED && resultCode == RESULT_OK && data != null) {
Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
if (Settings.System.DEFAULT_NOTIFICATION_URI.equals(uri)) {
Prefs.removeNotificationRingtone(getContext());
} else {
Prefs.setNotificationRingtone(getContext(), uri != null ? uri : Uri.EMPTY);
}
initializeRingtoneSummary(findPreference(Prefs.RINGTONE_PREF));
}
}
@Override
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
Context context = getContext();
@@ -44,6 +44,8 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
private static final String KEY_IMAGE_URI = "image_uri";
public static final int SELECT_STICKER_REQUEST_CODE = 123;
private EditorModel restoredModel;
@Nullable
@@ -193,6 +195,12 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
startTextEntityEditing(element, true);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
imageEditorHud.enterMode(ImageEditorHud.Mode.NONE);
}
@Override
public void onModeStarted(@NonNull ImageEditorHud.Mode mode) {
imageEditorView.setMode(ImageEditorView.Mode.MoveAndResize);
+161 -142
View File
@@ -18,50 +18,61 @@
<org.thoughtcrime.securesms.components.RecentPhotoViewRail
android:id="@+id/recent_photos"
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_height="160dp"
android:padding="4dp"/>
<LinearLayout android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="16dp"
android:weightSum="4">
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:scrollbars="horizontal">
<!-- first row -->
<LinearLayout
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="16dp"
android:paddingLeft="8dp"
android:paddingRight="8dp">
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<LinearLayout
android:layout_width="90dp"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:paddingLeft="8dp"
android:paddingRight="8dp">
<org.thoughtcrime.securesms.components.CircleColorImageView
android:id="@+id/record_video_button"
android:layout_width="53dp"
android:layout_height="53dp"
android:src="@drawable/attach_record_video"
android:scaleType="center"
android:contentDescription="@string/video"
app:circleColor="#00FFFFFF"/>
<org.thoughtcrime.securesms.components.CircleColorImageView
android:id="@+id/record_video_button"
android:layout_width="53dp"
android:layout_height="53dp"
android:src="@drawable/attach_record_video"
android:scaleType="center"
android:contentDescription="@string/video"
app:circleColor="#00FFFFFF"/>
<TextView android:layout_marginTop="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
style="@style/AttachmentTypeLabel"
android:text="@string/video"/>
<TextView
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:ellipsize="end"
android:maxLines="1"
style="@style/AttachmentTypeLabel"
android:text="@string/video"/>
</LinearLayout>
</LinearLayout>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<LinearLayout
android:layout_width="90dp"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:paddingLeft="8dp"
android:paddingRight="8dp">
<org.thoughtcrime.securesms.components.CircleColorImageView
<org.thoughtcrime.securesms.components.CircleColorImageView
android:id="@+id/gallery_button"
android:layout_width="53dp"
android:layout_height="53dp"
@@ -70,133 +81,141 @@
android:contentDescription="@string/gallery"
app:circleColor="@color/gallery_icon"/>
<TextView android:layout_marginTop="10dp"
style="@style/AttachmentTypeLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/gallery"/>
<TextView
android:layout_marginTop="10dp"
style="@style/AttachmentTypeLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/gallery"/>
</LinearLayout>
</LinearLayout>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_weight="1"
android:orientation="vertical">
<LinearLayout
android:layout_width="90dp"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:paddingLeft="8dp"
android:paddingRight="8dp">
<org.thoughtcrime.securesms.components.CircleColorImageView
android:id="@+id/document_button"
android:layout_width="53dp"
android:layout_height="53dp"
android:src="@drawable/ic_insert_drive_file_white_24dp"
android:scaleType="center"
android:contentDescription="@string/file"
app:circleColor="@color/document_icon"/>
<org.thoughtcrime.securesms.components.CircleColorImageView
android:id="@+id/document_button"
android:layout_width="53dp"
android:layout_height="53dp"
android:src="@drawable/ic_insert_drive_file_white_24dp"
android:scaleType="center"
android:contentDescription="@string/file"
app:circleColor="@color/document_icon"/>
<TextView android:layout_marginTop="10dp"
style="@style/AttachmentTypeLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/file"/>
<TextView
android:layout_marginTop="10dp"
style="@style/AttachmentTypeLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/file"/>
</LinearLayout>
</LinearLayout>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<LinearLayout
android:layout_width="90dp"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:paddingLeft="8dp"
android:paddingRight="8dp">
<org.thoughtcrime.securesms.components.CircleColorImageView
<org.thoughtcrime.securesms.components.CircleColorImageView
android:id="@+id/webxdc_button"
android:layout_width="53dp"
android:layout_height="53dp"
android:src="@drawable/ic_apps_24"
android:scaleType="center"
android:contentDescription="@string/webxdc_app"
app:circleColor="@color/apps_icon"
/>
app:circleColor="@color/apps_icon"/>
<TextView android:layout_marginTop="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/AttachmentTypeLabel"
android:text="@string/webxdc_app"/>
<TextView
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:ellipsize="end"
android:maxLines="1"
style="@style/AttachmentTypeLabel"
android:text="@string/webxdc_app"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
android:weightSum="4">
<!-- second row -->
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:layout_weight="1">
<org.thoughtcrime.securesms.components.CircleColorImageView
android:id="@+id/contact_button"
android:layout_width="53dp"
android:layout_height="53dp"
android:src="@drawable/ic_person_white_24dp"
android:scaleType="center"
android:contentDescription="@string/contact"
app:circleColor="@color/contact_icon"/>
<TextView android:layout_marginTop="10dp"
android:layout_width="wrap_content"
<LinearLayout
android:layout_width="90dp"
android:layout_height="wrap_content"
style="@style/AttachmentTypeLabel"
android:text="@string/contact"/>
android:gravity="center"
android:orientation="vertical"
android:paddingLeft="8dp"
android:paddingRight="8dp">
<org.thoughtcrime.securesms.components.CircleColorImageView
android:id="@+id/contact_button"
android:layout_width="53dp"
android:layout_height="53dp"
android:src="@drawable/ic_person_white_24dp"
android:scaleType="center"
android:contentDescription="@string/contact"
app:circleColor="@color/contact_icon"/>
<TextView
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:ellipsize="end"
android:maxLines="1"
style="@style/AttachmentTypeLabel"
android:text="@string/contact"/>
</LinearLayout>
<LinearLayout
android:id="@+id/location_linear_layout"
android:layout_width="90dp"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:paddingLeft="8dp"
android:paddingRight="8dp">
<org.thoughtcrime.securesms.components.CircleColorImageView
android:id="@+id/location_button"
android:layout_width="53dp"
android:layout_height="53dp"
android:src="@drawable/ic_location_on_white_24dp"
android:scaleType="center"
android:visibility="visible"
android:contentDescription="@string/location"
app:circleColor="@color/location_icon"/>
<TextView
android:layout_marginTop="10dp"
android:id="@+id/location_button_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="visible"
android:gravity="center"
android:ellipsize="end"
android:maxLines="1"
style="@style/AttachmentTypeLabel"
android:text="@string/location"/>
</LinearLayout>
</LinearLayout>
<LinearLayout android:id="@+id/location_linear_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<org.thoughtcrime.securesms.components.CircleColorImageView
android:id="@+id/location_button"
android:layout_width="53dp"
android:layout_height="53dp"
android:src="@drawable/ic_location_on_white_24dp"
android:scaleType="center"
android:visibility="visible"
android:contentDescription="@string/location"
app:circleColor="@color/location_icon"/>
<TextView android:layout_marginTop="10dp"
android:id="@+id/location_button_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
style="@style/AttachmentTypeLabel"
android:text="@string/location"/>
</LinearLayout>
<!-- fill the gap -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:visibility="invisible">
</LinearLayout>
</LinearLayout>
</HorizontalScrollView>
</LinearLayout>
@@ -53,6 +53,14 @@
android:paddingEnd="16dp"
android:focusable="true" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/mention_suggestions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:background="?attr/input_panel_bg_color"
android:elevation="4dp" />
<include layout="@layout/conversation_input_panel"/>
<FrameLayout
+36
View File
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="?attr/conversation_list_item_background"
android:paddingStart="12dp"
android:paddingEnd="12dp">
<org.thoughtcrime.securesms.components.AvatarImageView
android:id="@+id/mention_avatar"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginEnd="10dp" />
<TextView
android:id="@+id/mention_display_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:singleLine="true"
android:ellipsize="end"
android:textSize="15sp" />
<TextView
android:id="@+id/mention_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:textSize="13sp"
android:paddingStart="8dp"
android:textAppearance="?android:attr/textAppearanceSmall" />
</LinearLayout>