mirror of
https://github.com/ArcaneChat/android.git
synced 2026-07-03 14:05:24 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b6ef372c22 | |||
| 19e6d4c303 |
@@ -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:** 36 (Android 16)
|
||||
- **Target SDK:** 35 (Android 15)
|
||||
- **NDK Version:** 27.0.12077973
|
||||
- **Native Components:** Rust (deltachat-core-rust submodule)
|
||||
- **UI Framework:** Android SDK, Material Design Components
|
||||
@@ -17,23 +17,13 @@ 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, **don't edit directly**)
|
||||
- `jni/deltachat-core-rust/` - Native Rust core library (submodule)
|
||||
- `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
|
||||
|
||||
@@ -43,36 +33,17 @@ 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. **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:**
|
||||
2. **Build native libraries:**
|
||||
```bash
|
||||
scripts/ndk-make.sh
|
||||
```
|
||||
**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
|
||||
```
|
||||
Note: First run may take significant time as it builds for all architectures (armeabi-v7a, arm64-v8a, x86, x86_64)
|
||||
|
||||
5. **Build APK:**
|
||||
3. **Build APK:**
|
||||
```bash
|
||||
./gradlew assembleDebug
|
||||
```
|
||||
Build time: ~2-5 minutes after native libraries are built.
|
||||
|
||||
### Build Flavors
|
||||
|
||||
@@ -84,24 +55,6 @@ 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
|
||||
@@ -109,19 +62,16 @@ 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
|
||||
|
||||
@@ -179,14 +129,6 @@ 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)
|
||||
@@ -200,43 +142,6 @@ 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:
|
||||
@@ -247,24 +152,12 @@ ArcaneChat has extended WebXDC support:
|
||||
|
||||
## Important Files
|
||||
|
||||
- `build.gradle` - Main build configuration (Android Gradle Plugin 8.11.1, Java 8 compatibility)
|
||||
- `build.gradle` - Main build configuration
|
||||
- `CONTRIBUTING.md` - Contribution guidelines
|
||||
- `BUILDING.md` - Detailed build setup instructions
|
||||
- `BUILDING.md` - Detailed build setup
|
||||
- `RELEASE.md` - Release process
|
||||
- `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
|
||||
- `proguard-rules.pro` - ProGuard configuration
|
||||
- `google-services.json` - Firebase configuration (gplay flavor)
|
||||
|
||||
## Package Structure
|
||||
|
||||
@@ -280,35 +173,3 @@ 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
|
||||
|
||||
@@ -210,9 +210,6 @@ dependencies {
|
||||
implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
|
||||
implementation "io.noties.markwon:inline-parser:$markwon_version"
|
||||
implementation 'com.airbnb.android:lottie:4.2.2' // Lottie animations support.
|
||||
implementation ('com.vdurmont:emoji-java:5.1.1') { // emoji dataset for auto-completion
|
||||
exclude group: 'org.json', module: 'json'
|
||||
}
|
||||
|
||||
def media3_version = "1.8.0" // 1.9.0 need minSdkVersion 23
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ 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;
|
||||
@@ -91,6 +93,7 @@ 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) {
|
||||
@@ -98,6 +101,22 @@ 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();
|
||||
}
|
||||
|
||||
@@ -314,7 +333,7 @@ public class ContactSelectionListFragment extends Fragment
|
||||
intent.putExtra(NewContactActivity.ADDR_EXTRA, cursorFilter);
|
||||
}
|
||||
if (isMulti()) {
|
||||
startActivityForResult(intent, CONTACT_ADDR_RESULT_CODE);
|
||||
newContactLauncher.launch(intent);
|
||||
} else {
|
||||
requireContext().startActivity(intent);
|
||||
}
|
||||
@@ -362,17 +381,4 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,6 @@ import org.thoughtcrime.securesms.components.ScaleStableImageView;
|
||||
import org.thoughtcrime.securesms.components.SendButton;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiSearch;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.connect.AccountManager;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
@@ -977,7 +976,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
container.addOnKeyboardShownListener(backgroundView);
|
||||
inputPanel.setListener(this);
|
||||
inputPanel.setMediaListener(this);
|
||||
inputPanel.setEmojiSuggestionListener(emoji -> insertEmojiSuggestion(emoji));
|
||||
|
||||
attachmentTypeSelector = null;
|
||||
attachmentManager = new AttachmentManager(this, this);
|
||||
@@ -1595,84 +1593,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
if (composeText.getTextTrimmed().length() == 0 || beforeLength == 0) {
|
||||
composeText.postDelayed(ConversationActivity.this::updateToggleButtonState, 50);
|
||||
}
|
||||
updateEmojiSuggestions(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before,int count) {}
|
||||
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
if (!hasFocus) inputPanel.hideEmojiSuggestions();
|
||||
}
|
||||
}
|
||||
|
||||
private String lastEmojiQuery = null;
|
||||
|
||||
private void updateEmojiSuggestions(CharSequence text) {
|
||||
String query = getEmojiQuery(text, composeText.getSelectionEnd());
|
||||
if (query == null) {
|
||||
lastEmojiQuery = null;
|
||||
inputPanel.hideEmojiSuggestions();
|
||||
return;
|
||||
}
|
||||
if (query.equals(lastEmojiQuery)) return;
|
||||
lastEmojiQuery = query;
|
||||
Util.runOnBackground(() -> {
|
||||
List<String> results = EmojiSearch.search(query);
|
||||
Util.runOnMain(() -> {
|
||||
if (!query.equals(lastEmojiQuery)) return; // stale result
|
||||
if (results.isEmpty()) {
|
||||
inputPanel.hideEmojiSuggestions();
|
||||
} else {
|
||||
inputPanel.showEmojiSuggestions(results);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the emoji search query if the cursor is positioned after a `:word` pattern,
|
||||
* or null if there is no active emoji query.
|
||||
*/
|
||||
private String getEmojiQuery(CharSequence text, int cursorPos) {
|
||||
if (text == null || cursorPos <= 0) return null;
|
||||
int colonPos = -1;
|
||||
for (int i = cursorPos - 1; i >= 0; i--) {
|
||||
char c = text.charAt(i);
|
||||
if (c == ':') {
|
||||
colonPos = i;
|
||||
break;
|
||||
}
|
||||
if (!Character.isLetterOrDigit(c) && c != '_') break;
|
||||
}
|
||||
if (colonPos < 0) return null;
|
||||
String query = text.subSequence(colonPos + 1, cursorPos).toString();
|
||||
if (query.length() < 2) return null;
|
||||
return query;
|
||||
}
|
||||
|
||||
private void insertEmojiSuggestion(String emoji) {
|
||||
android.text.Editable editable = composeText.getText();
|
||||
if (editable == null) return;
|
||||
int cursorPos = composeText.getSelectionEnd();
|
||||
if (cursorPos < 0) return;
|
||||
// Find colon before the query at the cursor position
|
||||
int colonPos = -1;
|
||||
for (int i = cursorPos - 1; i >= 0; i--) {
|
||||
char c = editable.charAt(i);
|
||||
if (c == ':') {
|
||||
colonPos = i;
|
||||
break;
|
||||
}
|
||||
if (!Character.isLetterOrDigit(c) && c != '_') break;
|
||||
}
|
||||
if (colonPos < 0) return;
|
||||
String query = editable.subSequence(colonPos + 1, cursorPos).toString();
|
||||
if (query.length() < 2) return;
|
||||
editable.replace(colonPos, cursorPos, emoji + " ");
|
||||
lastEmojiQuery = null;
|
||||
inputPanel.hideEmojiSuggestions();
|
||||
public void onFocusChange(View v, boolean hasFocus) {}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -87,7 +87,6 @@ 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();
|
||||
@@ -949,17 +948,6 @@ 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,6 +11,8 @@ 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;
|
||||
@@ -48,6 +50,7 @@ public class ProfileFragment extends Fragment
|
||||
private ActionMode actionMode;
|
||||
private final ActionModeCallback actionModeCallback = new ActionModeCallback();
|
||||
|
||||
private ActivityResultLauncher<Intent> pickContactLauncher;
|
||||
|
||||
private DcContext dcContext;
|
||||
protected int chatId;
|
||||
@@ -60,6 +63,41 @@ 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
|
||||
@@ -210,7 +248,7 @@ public class ProfileFragment extends Fragment
|
||||
preselectedContacts.add(memberId);
|
||||
}
|
||||
intent.putExtra(ContactSelectionListFragment.PRESELECTED_CONTACTS, preselectedContacts);
|
||||
startActivityForResult(intent, REQUEST_CODE_PICK_CONTACT);
|
||||
pickContactLauncher.launch(intent);
|
||||
}
|
||||
|
||||
public void onQrInvite() {
|
||||
@@ -304,36 +342,4 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,14 +21,11 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.b44t.messenger.DcMsg;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiSuggestionAdapter;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
@@ -40,7 +37,6 @@ import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
||||
import org.thoughtcrime.securesms.util.guava.Optional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@@ -66,8 +62,6 @@ public class InputPanel extends ConstraintLayout
|
||||
private View buttonToggle;
|
||||
private View recordingContainer;
|
||||
private View recordLockCancel;
|
||||
private RecyclerView emojiSuggestions;
|
||||
private EmojiSuggestionAdapter emojiSuggestionAdapter;
|
||||
|
||||
private MicrophoneRecorderView microphoneRecorderView;
|
||||
private SlideToCancel slideToCancel;
|
||||
@@ -109,11 +103,6 @@ public class InputPanel extends ConstraintLayout
|
||||
this.microphoneRecorderView = findViewById(R.id.recorder_view);
|
||||
this.microphoneRecorderView.setListener(this);
|
||||
|
||||
this.emojiSuggestions = findViewById(R.id.emoji_suggestions);
|
||||
this.emojiSuggestionAdapter = new EmojiSuggestionAdapter();
|
||||
this.emojiSuggestions.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false));
|
||||
this.emojiSuggestions.setAdapter(emojiSuggestionAdapter);
|
||||
|
||||
this.recordLockCancel.setOnClickListener(v -> microphoneRecorderView.cancelAction());
|
||||
|
||||
quoteDismiss.setOnClickListener(v -> clearQuote());
|
||||
@@ -240,20 +229,6 @@ public class InputPanel extends ConstraintLayout
|
||||
mediaKeyboard.setKeyboardListener(this);
|
||||
}
|
||||
|
||||
public void setEmojiSuggestionListener(@NonNull EmojiSuggestionAdapter.OnEmojiClickListener listener) {
|
||||
emojiSuggestionAdapter.setOnEmojiClickListener(listener);
|
||||
}
|
||||
|
||||
public void showEmojiSuggestions(@NonNull List<String> emojis) {
|
||||
emojiSuggestionAdapter.setEmojis(emojis);
|
||||
emojiSuggestions.setVisibility(View.VISIBLE);
|
||||
emojiSuggestions.scrollToPosition(0);
|
||||
}
|
||||
|
||||
public void hideEmojiSuggestions() {
|
||||
emojiSuggestions.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecordPermissionRequired() {
|
||||
if (listener != null) listener.onRecorderPermissionRequired();
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.vdurmont.emoji.Emoji;
|
||||
import com.vdurmont.emoji.EmojiManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Provides emoji search functionality using the emoji-java library.
|
||||
* Used for emoji auto-completion when user types :query in the compose field.
|
||||
*/
|
||||
public final class EmojiSearch {
|
||||
|
||||
private static final int MAX_RESULTS = 20;
|
||||
|
||||
private EmojiSearch() {}
|
||||
|
||||
/**
|
||||
* Search for emojis whose aliases or tags contain the given query (case-insensitive).
|
||||
* Returns up to MAX_RESULTS emoji Unicode strings.
|
||||
*/
|
||||
@NonNull
|
||||
public static List<String> search(@NonNull String query) {
|
||||
if (query.isEmpty()) return Collections.emptyList();
|
||||
String lowerQuery = query.toLowerCase();
|
||||
List<String> results = new ArrayList<>();
|
||||
for (Emoji emoji : EmojiManager.getAll()) {
|
||||
if (matchesQuery(emoji, lowerQuery)) {
|
||||
results.add(emoji.getUnicode());
|
||||
if (results.size() >= MAX_RESULTS) break;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private static boolean matchesQuery(@NonNull Emoji emoji, @NonNull String lowerQuery) {
|
||||
for (String alias : emoji.getAliases()) {
|
||||
if (alias.toLowerCase().contains(lowerQuery)) return true;
|
||||
}
|
||||
for (String tag : emoji.getTags()) {
|
||||
if (tag.toLowerCase().contains(lowerQuery)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
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 org.thoughtcrime.securesms.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* RecyclerView adapter for showing emoji auto-completion suggestions.
|
||||
*/
|
||||
public class EmojiSuggestionAdapter extends RecyclerView.Adapter<EmojiSuggestionAdapter.EmojiViewHolder> {
|
||||
|
||||
public interface OnEmojiClickListener {
|
||||
void onEmojiClicked(@NonNull String emoji);
|
||||
}
|
||||
|
||||
private final List<String> emojis = new ArrayList<>();
|
||||
private OnEmojiClickListener listener;
|
||||
|
||||
public void setOnEmojiClickListener(OnEmojiClickListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void setEmojis(@NonNull List<String> newEmojis) {
|
||||
int oldSize = emojis.size();
|
||||
emojis.clear();
|
||||
emojis.addAll(newEmojis);
|
||||
int newSize = emojis.size();
|
||||
if (oldSize > 0) notifyItemRangeRemoved(0, oldSize);
|
||||
if (newSize > 0) notifyItemRangeInserted(0, newSize);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public EmojiViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.emoji_suggestion_item, parent, false);
|
||||
return new EmojiViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull EmojiViewHolder holder, int position) {
|
||||
String emoji = emojis.get(position);
|
||||
holder.emojiView.setText(emoji);
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
if (listener != null) listener.onEmojiClicked(emoji);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return emojis.size();
|
||||
}
|
||||
|
||||
static class EmojiViewHolder extends RecyclerView.ViewHolder {
|
||||
final TextView emojiView;
|
||||
|
||||
EmojiViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
emojiView = itemView.findViewById(R.id.emoji_text);
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
-7
@@ -18,6 +18,8 @@ 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;
|
||||
@@ -55,11 +57,22 @@ 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) -> {
|
||||
@@ -158,8 +171,7 @@ public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
|
||||
Preference relayListBtn = this.findPreference("pref_relay_list_button");
|
||||
if (relayListBtn != null) {
|
||||
relayListBtn.setOnPreferenceClickListener(((preference) -> {
|
||||
boolean result = ScreenLockUtil.applyScreenLock(requireActivity(), getString(R.string.transports), getString(R.string.enter_system_secret_to_continue), REQUEST_CODE_CONFIRM_CREDENTIALS_ACCOUNT);
|
||||
if (!result) {
|
||||
if (!applyScreenLockWithLauncher()) {
|
||||
openRelayListActivity();
|
||||
}
|
||||
return true;
|
||||
@@ -191,12 +203,17 @@ public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
|
||||
onlyFetchMvboxCheckbox.setChecked(0!=dcContext.getConfigInt(CONFIG_ONLY_FETCH_MVBOX));
|
||||
}
|
||||
|
||||
@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();
|
||||
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;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected File copyToCacheDir(Uri uri) throws IOException {
|
||||
|
||||
@@ -10,6 +10,8 @@ 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;
|
||||
@@ -30,11 +32,22 @@ 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) -> {
|
||||
@@ -113,14 +126,6 @@ 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)) {
|
||||
@@ -131,6 +136,19 @@ 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
|
||||
**********************************************************************************************/
|
||||
@@ -138,8 +156,7 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
private class BackupListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(@NonNull Preference preference) {
|
||||
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) {
|
||||
if (!applyScreenLockWithLauncher()) {
|
||||
performBackup();
|
||||
}
|
||||
return true;
|
||||
|
||||
+22
-17
@@ -16,6 +16,8 @@ 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;
|
||||
@@ -40,11 +42,30 @@ 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)
|
||||
@@ -66,7 +87,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);
|
||||
|
||||
startActivityForResult(intent, REQUEST_CODE_NOTIFICATION_SELECTED);
|
||||
ringtonePickerLauncher.launch(intent);
|
||||
|
||||
return true;
|
||||
});
|
||||
@@ -137,22 +158,6 @@ 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,8 +44,6 @@ 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
|
||||
@@ -195,12 +193,6 @@ 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);
|
||||
|
||||
@@ -18,61 +18,50 @@
|
||||
<org.thoughtcrime.securesms.components.RecentPhotoViewRail
|
||||
android:id="@+id/recent_photos"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="160dp"
|
||||
android:layout_height="90dp"
|
||||
android:padding="4dp"/>
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:scrollbars="horizontal">
|
||||
<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">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="16dp"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="8dp">
|
||||
<!-- first row -->
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="90dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
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">
|
||||
|
||||
<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="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
style="@style/AttachmentTypeLabel"
|
||||
android:text="@string/video"/>
|
||||
<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"/>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="90dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
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">
|
||||
|
||||
<org.thoughtcrime.securesms.components.CircleColorImageView
|
||||
<org.thoughtcrime.securesms.components.CircleColorImageView
|
||||
android:id="@+id/gallery_button"
|
||||
android:layout_width="53dp"
|
||||
android:layout_height="53dp"
|
||||
@@ -81,141 +70,133 @@
|
||||
android:contentDescription="@string/gallery"
|
||||
app:circleColor="@color/gallery_icon"/>
|
||||
|
||||
<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"/>
|
||||
<TextView android:layout_marginTop="10dp"
|
||||
style="@style/AttachmentTypeLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/gallery"/>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="90dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="8dp">
|
||||
<LinearLayout android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<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="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@string/file"/>
|
||||
<TextView android:layout_marginTop="10dp"
|
||||
style="@style/AttachmentTypeLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/file"/>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="90dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
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">
|
||||
|
||||
<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="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
|
||||
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/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>
|
||||
<TextView android:layout_marginTop="10dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/AttachmentTypeLabel"
|
||||
android:text="@string/webxdc_app"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</HorizontalScrollView>
|
||||
</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"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/AttachmentTypeLabel"
|
||||
android:text="@string/contact"/>
|
||||
|
||||
</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>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -42,24 +42,11 @@
|
||||
app:layout_constraintTop_toBottomOf="@id/quote_view"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/emoji_suggestions"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:background="?attr/input_panel_bg_color"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/subject_text" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/input_field_frame_layout"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/button_toggle"
|
||||
app:layout_constraintTop_toBottomOf="@id/emoji_suggestions"
|
||||
app:layout_constraintTop_toBottomOf="@id/subject_text"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/emoji_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:minWidth="44dp"
|
||||
android:textSize="26sp"
|
||||
android:textColor="?attr/emoji_text_color"
|
||||
android:background="@drawable/touch_highlight_background" />
|
||||
Reference in New Issue
Block a user