diff --git a/.github/workflows/preview-apk.yml b/.github/workflows/preview-apk.yml index b8abccc7e..4b5a780c0 100644 --- a/.github/workflows/preview-apk.yml +++ b/.github/workflows/preview-apk.yml @@ -45,10 +45,10 @@ jobs: ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} run: | export PATH="${PATH}:${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/" - scripts/install-toolchains.sh && scripts/ndk-make.sh armeabi-v7a + scripts/install-toolchains.sh && scripts/ndk-make.sh arm64-v8a - name: Build APK - run: ./gradlew --no-daemon -PABI_FILTER=armeabi-v7a assembleFossDebug + run: ./gradlew --no-daemon -PABI_FILTER=arm64-v8a assembleFossDebug - name: Upload APK uses: actions/upload-artifact@v4 diff --git a/CHANGELOG-upstream.md b/CHANGELOG-upstream.md index 54abfb28e..523440fd8 100644 --- a/CHANGELOG-upstream.md +++ b/CHANGELOG-upstream.md @@ -3,8 +3,12 @@ ## Unreleased * Allow to set chat description +* Unified date display in call bubbles +* Explain at "Settings / Chats / Outgoing Media Quality" how to send original quality +* Add a basic sticker picker * Fix: keep original sent timestamp for resent messages * Fix: make clicking on broadcast member-added messages work always +* Fix: remove notification when a message is deleted by sender * Update to core 2.44.0 ## v2.43.0 diff --git a/src/main/java/com/b44t/messenger/DcContext.java b/src/main/java/com/b44t/messenger/DcContext.java index 819f7b75b..27132c2a4 100644 --- a/src/main/java/com/b44t/messenger/DcContext.java +++ b/src/main/java/com/b44t/messenger/DcContext.java @@ -15,6 +15,7 @@ public class DcContext { public final static int DC_EVENT_MSG_DELIVERED = 2010; public final static int DC_EVENT_MSG_FAILED = 2012; public final static int DC_EVENT_MSG_READ = 2015; + public final static int DC_EVENT_MSG_DELETED = 2016; public final static int DC_EVENT_CHAT_MODIFIED = 2020; public final static int DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021; public final static int DC_EVENT_CHAT_DELETED = 2023; diff --git a/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java b/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java index 33f49a78a..863cf2c17 100644 --- a/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java +++ b/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java @@ -33,5 +33,6 @@ public interface BindableConversationItem extends Unbindable { void onShowFullClicked(DcMsg messageRecord); void onDownloadClicked(DcMsg messageRecord); void onReactionClicked(DcMsg messageRecord); + void onStickerClicked(DcMsg messageRecord); } } diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationActivity.java b/src/main/java/org/thoughtcrime/securesms/ConversationActivity.java index 428c4731a..608ee4053 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationActivity.java @@ -1456,6 +1456,18 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity isEditing = false; } + @Override + public void onStickerPicked(Uri stickerUri) { + if (isEditing) return; + sendSticker(stickerUri, "image/webp"); + } + + public void refreshStickerPicker() { + if (emojiPicker != null) { + emojiPicker.refreshStickerPicker(); + } + } + // media selected by the system keyboard @Override public void onMediaSelected(@NonNull Uri uri, String contentType) { diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java b/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java index c46ad388a..beecb4160 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java @@ -39,6 +39,7 @@ import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.view.ActionMode; import androidx.lifecycle.ViewModelProvider; @@ -70,6 +71,7 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.views.ConversationAdaptiveActionsToolbar; +import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; @@ -553,6 +555,51 @@ public class ConversationFragment extends MessageSelectorFragment } } + private void handleSaveSticker(final DcMsg message) { + if (message.getType() != DcMsg.DC_MSG_STICKER) { + return; + } + + File stickerFile = message.getFileAsFile(); + if (stickerFile == null || !stickerFile.exists()) { + Toast.makeText(getContext(), R.string.error, Toast.LENGTH_SHORT).show(); + return; + } + + // Create stickers directory in internal storage + File stickersDir = new File(getContext().getFilesDir(), "stickers"); + if (!stickersDir.exists()) { + stickersDir.mkdirs(); + } + + // Copy sticker to stickers directory + String extension = stickerFile.getName(); + int dotPos = extension.lastIndexOf('.'); + if (dotPos >= 0) { + extension = extension.substring(dotPos + 1); + } + String fileName = System.currentTimeMillis() + "." + extension; + File destFile = new File(stickersDir, fileName); + + try (java.io.FileInputStream in = new java.io.FileInputStream(stickerFile); + java.io.FileOutputStream out = new java.io.FileOutputStream(destFile)) { + byte[] buffer = new byte[1024]; + int read; + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + Toast.makeText(getContext(), R.string.saved, Toast.LENGTH_SHORT).show(); + + // Refresh sticker picker to show the newly added sticker + if (getActivity() instanceof ConversationActivity) { + ((ConversationActivity) getActivity()).refreshStickerPicker(); + } + } catch (Exception e) { + Log.e(TAG, "Error saving sticker", e); + Toast.makeText(getContext(), R.string.error, Toast.LENGTH_SHORT).show(); + } + } + @SuppressLint("RestrictedApi") private void handleReplyMessage(final DcMsg message) { if (getActivity() != null) { @@ -951,6 +998,18 @@ public class ConversationFragment extends MessageSelectorFragment ReactionsDetailsFragment dialog = ReactionsDetailsFragment.newInstance(messageRecord.getId()); dialog.show(getActivity().getSupportFragmentManager(), null); } + + @Override + public void onStickerClicked(DcMsg messageRecord) { + new AlertDialog.Builder(getContext()) + .setTitle(R.string.add_to_sticker_collection) + .setMessage(R.string.ask_add_sticker_to_collection) + .setPositiveButton(R.string.ok, (dialog, which) -> { + handleSaveSticker(messageRecord); + }) + .setNegativeButton(R.string.cancel, null) + .show(); + } } private class ActionModeCallback implements ActionMode.Callback { diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationItem.java b/src/main/java/org/thoughtcrime/securesms/ConversationItem.java index 345fc8fdd..8b2271e2b 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationItem.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationItem.java @@ -807,8 +807,6 @@ public class ConversationItem extends BaseConversationItem return stickerStub.get().getFooter(); } else if (hasOnlyThumbnail(messageRecord) && TextUtils.isEmpty(messageRecord.getText())) { return mediaThumbnailStub.get().getFooter(); - } else if (messageRecord.getType() == DcMsg.DC_MSG_CALL) { - return callViewStub.get().getFooter(); } else { return footer; } @@ -969,6 +967,8 @@ public class ConversationItem extends BaseConversationItem public void onClick(final View v, final Slide slide) { if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) { performClick(); + } else if (eventListener != null) { + eventListener.onStickerClicked(messageRecord); } } } diff --git a/src/main/java/org/thoughtcrime/securesms/components/CallItemView.java b/src/main/java/org/thoughtcrime/securesms/components/CallItemView.java index 3ab0672ce..aa9dbf9e9 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/CallItemView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/CallItemView.java @@ -12,6 +12,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.DateUtils; import chat.delta.rpc.types.CallInfo; import chat.delta.rpc.types.CallState; @@ -21,7 +22,7 @@ public class CallItemView extends FrameLayout { private final @NonNull ImageView icon; private final @NonNull TextView title; - private final @NonNull ConversationItemFooter footer; + private final @NonNull TextView duration; private CallInfo callInfo; private CallClickListener viewListener; @@ -40,7 +41,7 @@ public class CallItemView extends FrameLayout { this.icon = findViewById(R.id.call_icon); this.title = findViewById(R.id.title); - this.footer = findViewById(R.id.footer); + this.duration = findViewById(R.id.duration); setOnClickListener(v -> { if (viewListener != null && callInfo != null) { @@ -55,10 +56,12 @@ public class CallItemView extends FrameLayout { public void setCallItem(boolean isOutgoing, CallInfo callInfo) { this.callInfo = callInfo; + if (callInfo.state instanceof CallState.Completed) { - footer.setCallDuration(((CallState.Completed) callInfo.state).duration); + duration.setText(DateUtils.getFormattedCallDuration(getContext(), ((CallState.Completed) callInfo.state).duration)); + duration.setVisibility(VISIBLE); } else { - footer.setCallDuration(0); // reset + duration.setVisibility(GONE); } if (callInfo.state instanceof CallState.Missed) { @@ -79,26 +82,19 @@ public class CallItemView extends FrameLayout { if (isOutgoing) { attrs = new int[]{ R.attr.conversation_item_outgoing_text_primary_color, - R.attr.conversation_item_outgoing_text_secondary_color, }; } else { attrs = new int[]{ R.attr.conversation_item_incoming_text_primary_color, - R.attr.conversation_item_incoming_text_secondary_color, }; } try (TypedArray ta = getContext().obtainStyledAttributes(attrs)) { icon.setColorFilter(ta.getColor(0, Color.BLACK)); - footer.setTextColor(ta.getColor(1, Color.BLACK)); } } - public ConversationItemFooter getFooter() { - return footer; - } - public String getDescription() { - return title.getText() + "\n" + footer.getDescription(); + return title.getText() + (duration.getVisibility()==VISIBLE ? ("\n" + duration.getText()) : ""); } public interface CallClickListener { diff --git a/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java b/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java index 615364d8e..33f01f8df 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java +++ b/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java @@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.components; import android.content.Context; import android.content.res.TypedArray; -import android.graphics.Color; import android.util.AttributeSet; import android.view.View; import android.widget.ImageView; @@ -32,7 +31,6 @@ public class ConversationItemFooter extends LinearLayout { private ImageView locationIndicatorView; private DeliveryStatusView deliveryStatusView; private Integer textColor = null; - private int callDuration = 0; private Context context; private Rpc rpc; @@ -72,11 +70,6 @@ public class ConversationItemFooter extends LinearLayout { } } - /* Call duration in seconds. Only >0 if this is a call message */ - public void setCallDuration(int duration) { - callDuration = duration; - } - public void setMessageRecord(@NonNull DcMsg messageRecord) { presentDate(messageRecord); boolean bookmark = messageRecord.getOriginalMsgId() != 0 || messageRecord.getSavedMsgId() != 0; @@ -112,7 +105,7 @@ public class ConversationItemFooter extends LinearLayout { presentDeliveryStatus(messageRecord, isOutChannel); } - public void setTextColor(int color) { + private void setTextColor(int color) { textColor = color; dateView.setTextColor(color); editedView.setTextColor(color); @@ -126,27 +119,17 @@ public class ConversationItemFooter extends LinearLayout { private void presentDate(@NonNull DcMsg dcMsg) { dateView.forceLayout(); - Context context = getContext(); - String date = dcMsg.getType() == DcMsg.DC_MSG_CALL? - DateUtils.getExtendedTimeSpanString(context, dcMsg.getTimestamp()) - : DateUtils.getExtendedRelativeTimeSpanString(context, dcMsg.getTimestamp()); - if (callDuration > 0) { - String duration = DateUtils.getFormattedCallDuration(context, callDuration); - dateView.setText(context.getString(R.string.call_date_and_duration, date, duration)); - } else { - dateView.setText(date); - } + dateView.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), dcMsg.getTimestamp())); } private void presentDeliveryStatus(@NonNull DcMsg messageRecord, boolean isOutChannel) { // isDownloading is temporary and should be checked first. boolean isDownloading = messageRecord.getDownloadState() == DcMsg.DC_DOWNLOAD_IN_PROGRESS; - boolean isCall = messageRecord.getType() == DcMsg.DC_MSG_CALL; if (isDownloading) deliveryStatusView.setDownloading(); else if (messageRecord.isPending()) deliveryStatusView.setPending(); else if (messageRecord.isFailed()) deliveryStatusView.setFailed(); - else if (!messageRecord.isOutgoing() || isCall || isOutChannel) deliveryStatusView.setNone(); + else if (!messageRecord.isOutgoing() || isOutChannel) deliveryStatusView.setNone(); else if (messageRecord.isRemoteRead()) deliveryStatusView.setRead(); else if (messageRecord.isDelivered()) deliveryStatusView.setSent(); else if (messageRecord.isPreparing()) deliveryStatusView.setPreparing(); diff --git a/src/main/java/org/thoughtcrime/securesms/components/CustomDefaultPreference.java b/src/main/java/org/thoughtcrime/securesms/components/CustomDefaultPreference.java deleted file mode 100644 index b3a2e2655..000000000 --- a/src/main/java/org/thoughtcrime/securesms/components/CustomDefaultPreference.java +++ /dev/null @@ -1,209 +0,0 @@ -package org.thoughtcrime.securesms.components; - -import android.app.Dialog; -import android.content.Context; -import android.content.res.TypedArray; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.preference.DialogPreference; -import androidx.preference.PreferenceDialogFragmentCompat; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.util.AttributeSet; -import android.util.Log; -import android.view.View; -import android.widget.AdapterView; -import android.widget.Button; -import android.widget.EditText; -import android.widget.Spinner; -import android.widget.TextView; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.components.CustomDefaultPreference.CustomDefaultPreferenceDialogFragmentCompat.CustomPreferenceValidator; -import org.thoughtcrime.securesms.util.Prefs; - -public class CustomDefaultPreference extends DialogPreference { - - private static final String TAG = CustomDefaultPreference.class.getSimpleName(); - - private final int inputType; - private final String customPreference; - private final String customToggle; - - private final CustomPreferenceValidator validator; - private String defaultValue; - - public CustomDefaultPreference(Context context, AttributeSet attrs) { - super(context, attrs); - - int[] attributeNames = new int[]{android.R.attr.inputType, R.attr.custom_pref_toggle}; - try (TypedArray attributes = context.obtainStyledAttributes(attrs, attributeNames)) { - this.inputType = attributes.getInt(0, 0); - this.customPreference = getKey(); - this.customToggle = attributes.getString(1); - this.validator = new CustomDefaultPreferenceDialogFragmentCompat.NullValidator(); - } - - setPersistent(false); - setDialogLayoutResource(R.layout.custom_default_preference_dialog); - } - - public CustomDefaultPreference setDefaultValue(String defaultValue) { - this.defaultValue = defaultValue; - this.setSummary(getSummary()); - return this; - } - - @Override - public String getSummary() { - if (isCustom()) { - return getContext().getString(R.string.pref_using_custom, - getPrettyPrintValue(getCustomValue())); - } else { - return getContext().getString(R.string.pref_using_default, - getPrettyPrintValue(getDefaultValue())); - } - } - - private String getPrettyPrintValue(String value) { - if (TextUtils.isEmpty(value)) return getContext().getString(R.string.none); - else return value; - } - - private boolean isCustom() { - return Prefs.getBooleanPreference(getContext(), customToggle, false); - } - - private void setCustom(boolean custom) { - Prefs.setBooleanPreference(getContext(), customToggle, custom); - } - - private String getCustomValue() { - return Prefs.getStringPreference(getContext(), customPreference, ""); - } - - private void setCustomValue(String value) { - Prefs.setStringPreference(getContext(), customPreference, value); - } - - private String getDefaultValue() { - return defaultValue; - } - - - public static class CustomDefaultPreferenceDialogFragmentCompat extends PreferenceDialogFragmentCompat { - - private static final String INPUT_TYPE = "input_type"; - - private Spinner spinner; - private EditText customText; - private TextView defaultLabel; - - public static CustomDefaultPreferenceDialogFragmentCompat newInstance(String key) { - CustomDefaultPreferenceDialogFragmentCompat fragment = new CustomDefaultPreferenceDialogFragmentCompat(); - Bundle b = new Bundle(1); - b.putString(PreferenceDialogFragmentCompat.ARG_KEY, key); - fragment.setArguments(b); - return fragment; - } - - @Override - protected void onBindDialogView(@NonNull View view) { - Log.w(TAG, "onBindDialogView"); - super.onBindDialogView(view); - - CustomDefaultPreference preference = (CustomDefaultPreference)getPreference(); - - this.spinner = (Spinner) view.findViewById(R.id.default_or_custom); - this.defaultLabel = (TextView) view.findViewById(R.id.default_label); - this.customText = (EditText) view.findViewById(R.id.custom_edit); - - this.customText.setInputType(preference.inputType); - this.customText.addTextChangedListener(new TextValidator()); - this.customText.setText(preference.getCustomValue()); - this.spinner.setOnItemSelectedListener(new SelectionLister()); - this.defaultLabel.setText(preference.getPrettyPrintValue(preference.defaultValue)); - } - - - @NonNull - @Override - public Dialog onCreateDialog(Bundle instanceState) { - Dialog dialog = super.onCreateDialog(instanceState); - - CustomDefaultPreference preference = (CustomDefaultPreference)getPreference(); - - if (preference.isCustom()) spinner.setSelection(1, true); - else spinner.setSelection(0, true); - - return dialog; - } - - @Override - public void onDialogClosed(boolean positiveResult) { - CustomDefaultPreference preference = (CustomDefaultPreference)getPreference(); - - if (positiveResult) { - if (spinner != null) preference.setCustom(spinner.getSelectedItemPosition() == 1); - if (customText != null) preference.setCustomValue(customText.getText().toString()); - - preference.setSummary(preference.getSummary()); - } - } - - interface CustomPreferenceValidator { - public boolean isValid(String value); - } - - private static class NullValidator implements CustomPreferenceValidator { - @Override - public boolean isValid(String value) { - return true; - } - } - - private class TextValidator implements TextWatcher { - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) {} - - @Override - public void afterTextChanged(Editable s) { - CustomDefaultPreference preference = (CustomDefaultPreference)getPreference(); - - if (spinner.getSelectedItemPosition() == 1) { - Button positiveButton = ((AlertDialog)getDialog()).getButton(AlertDialog.BUTTON_POSITIVE); - positiveButton.setEnabled(preference.validator.isValid(s.toString())); - } - } - } - - private class SelectionLister implements AdapterView.OnItemSelectedListener { - - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - CustomDefaultPreference preference = (CustomDefaultPreference)getPreference(); - Button positiveButton = ((AlertDialog)getDialog()).getButton(AlertDialog.BUTTON_POSITIVE); - - defaultLabel.setVisibility(position == 0 ? View.VISIBLE : View.GONE); - customText.setVisibility(position == 0 ? View.GONE : View.VISIBLE); - positiveButton.setEnabled(position == 0 || preference.validator.isValid(customText.getText().toString())); - } - - @Override - public void onNothingSelected(AdapterView parent) { - defaultLabel.setVisibility(View.VISIBLE); - customText.setVisibility(View.GONE); - } - } - - } - - - -} diff --git a/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java b/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java index 967203c6e..4c0895b1d 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java +++ b/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java @@ -353,6 +353,13 @@ public class InputPanel extends ConstraintLayout composeText.setSelection(start + emoji.length()); } + @Override + public void onStickerPicked(Uri stickerUri) { + if (listener != null) { + listener.onStickerPicked(stickerUri); + } + } + public interface Listener { void onRecorderStarted(); void onRecorderLocked(); @@ -361,6 +368,7 @@ public class InputPanel extends ConstraintLayout void onRecorderPermissionRequired(); void onEmojiToggle(); void onQuoteDismissed(); + void onStickerPicked(Uri stickerUri); } private static class SlideToCancel { diff --git a/src/main/java/org/thoughtcrime/securesms/components/emoji/MediaKeyboard.java b/src/main/java/org/thoughtcrime/securesms/components/emoji/MediaKeyboard.java index 6bc3a8fd7..1f83d3c56 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/emoji/MediaKeyboard.java +++ b/src/main/java/org/thoughtcrime/securesms/components/emoji/MediaKeyboard.java @@ -1,10 +1,13 @@ package org.thoughtcrime.securesms.components.emoji; import android.content.Context; +import android.graphics.drawable.Drawable; +import android.net.Uri; import android.util.AttributeSet; import android.util.Log; +import android.view.View; import android.view.ViewGroup; -import android.widget.FrameLayout; +import android.widget.LinearLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -12,15 +15,23 @@ import androidx.core.util.Consumer; import androidx.emoji2.emojipicker.EmojiPickerView; import androidx.emoji2.emojipicker.EmojiViewItem; +import com.google.android.material.tabs.TabLayout; + import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.InputAwareLayout.InputView; +import org.thoughtcrime.securesms.util.ResUtil; -public class MediaKeyboard extends FrameLayout implements InputView, Consumer { +import java.io.File; + +public class MediaKeyboard extends LinearLayout implements InputView, Consumer, StickerPickerView.StickerPickerListener { private static final String TAG = MediaKeyboard.class.getSimpleName(); - @Nullable private MediaKeyboardListener keyboardListener; + @Nullable private MediaKeyboardListener keyboardListener; private EmojiPickerView emojiPicker; + private StickerPickerView stickerPicker; + private View stickerPickerContainer; + private View stickerPickerEmpty; public MediaKeyboard(@NonNull Context context) { super(context); @@ -30,6 +41,51 @@ public class MediaKeyboard extends FrameLayout implements InputView, Consumer 0; + stickerPickerEmpty.setVisibility(hasStickers ? View.GONE : View.VISIBLE); + } + } + + public void refreshStickerPicker() { + if (stickerPicker != null) { + stickerPicker.loadStickers(); + updateStickerEmptyState(); + } + } + @Override public void hide(boolean immediate) { setVisibility(GONE); @@ -70,9 +158,22 @@ public class MediaKeyboard extends FrameLayout implements InputView, Consumer getSavedStickers() { + List stickerFiles = new ArrayList<>(); + + if (!stickerDir.exists()) { + return stickerFiles; + } + + File[] files = stickerDir.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isFile()) { + stickerFiles.add(file); + } + } + } + + // Sort stickers just to provide consistent order + Collections.sort(stickerFiles, (f1, f2) -> f1.getName().compareToIgnoreCase(f2.getName())); + + return stickerFiles; + } + + public interface StickerPickerListener { + void onStickerSelected(@NonNull File stickerFile); + void onStickerDeleted(@NonNull File stickerFile); + } + + static class StickerAdapter extends RecyclerView.Adapter { + + private final Context context; + private final GlideRequests glideRequests; + private final LayoutInflater layoutInflater; + private StickerPickerListener listener; + private List stickerFiles = new ArrayList<>(); + + StickerAdapter(@NonNull Context context) { + this.context = context; + this.glideRequests = GlideApp.with(context); + this.layoutInflater = LayoutInflater.from(context); + } + + public void changeData(@NonNull List stickerFiles) { + this.stickerFiles = stickerFiles; + notifyDataSetChanged(); + } + + public void setStickerPickerListener(@Nullable StickerPickerListener listener) { + this.listener = listener; + } + + @Override + public @NonNull StickerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = layoutInflater.inflate(R.layout.sticker_picker_item, parent, false); + return new StickerViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull StickerViewHolder holder, int position) { + File stickerFile = stickerFiles.get(position); + holder.stickerFile = stickerFile; + + glideRequests.load(stickerFile) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .into(holder.image); + } + + @Override + public int getItemCount() { + return stickerFiles.size(); + } + + @Override + public void onViewRecycled(@NonNull StickerViewHolder holder) { + super.onViewRecycled(holder); + glideRequests.clear(holder.image); + } + + private void deleteSticker(File stickerFile) { + if (stickerFile != null && stickerFile.exists()) { + new androidx.appcompat.app.AlertDialog.Builder(context) + .setTitle(R.string.delete) + .setMessage(R.string.ask_delete_sticker) + .setPositiveButton(R.string.delete, (dialog, which) -> { + if (stickerFile.delete()) { + int position = stickerFiles.indexOf(stickerFile); + if (position >= 0) { + stickerFiles.remove(position); + notifyItemRemoved(position); + } + if (listener != null) { + listener.onStickerDeleted(stickerFile); + } + } + }) + .setNegativeButton(R.string.cancel, null) + .show(); + } + } + + class StickerViewHolder extends RecyclerView.ViewHolder { + + private File stickerFile; + private final ImageView image; + + StickerViewHolder(View itemView) { + super(itemView); + image = itemView.findViewById(R.id.sticker_image); + itemView.setOnClickListener(view -> { + if (listener != null && stickerFile != null) { + listener.onStickerSelected(stickerFile); + } + }); + itemView.setOnLongClickListener(view -> { + if (stickerFile != null) { + deleteSticker(stickerFile); + return true; + } + return false; + }); + } + } + } +} diff --git a/src/main/java/org/thoughtcrime/securesms/connect/DcEventCenter.java b/src/main/java/org/thoughtcrime/securesms/connect/DcEventCenter.java index f766a2e53..9e194bf49 100644 --- a/src/main/java/org/thoughtcrime/securesms/connect/DcEventCenter.java +++ b/src/main/java/org/thoughtcrime/securesms/connect/DcEventCenter.java @@ -213,6 +213,10 @@ public class DcEventCenter { DcHelper.getNotificationCenter(context).removeNotifications(accountId, event.getData1Int()); break; + case DcContext.DC_EVENT_MSG_DELETED: + DcHelper.getNotificationCenter(context).removeNotification(accountId, event.getData1Int(), event.getData2Int()); + break; + case DcContext.DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE: FetchForegroundService.stop(context); break; diff --git a/src/main/java/org/thoughtcrime/securesms/connect/DcHelper.java b/src/main/java/org/thoughtcrime/securesms/connect/DcHelper.java index 8b9ed10e7..c9082e2d2 100644 --- a/src/main/java/org/thoughtcrime/securesms/connect/DcHelper.java +++ b/src/main/java/org/thoughtcrime/securesms/connect/DcHelper.java @@ -146,7 +146,6 @@ public class DcHelper { dcContext.setStockTranslation(100, context.getString(R.string.download_max_available_until)); dcContext.setStockTranslation(103, context.getString(R.string.incoming_messages)); dcContext.setStockTranslation(104, context.getString(R.string.outgoing_messages)); - dcContext.setStockTranslation(105, context.getString(R.string.storage_on_domain)); dcContext.setStockTranslation(107, context.getString(R.string.connectivity_connected)); dcContext.setStockTranslation(108, context.getString(R.string.connectivity_connecting)); dcContext.setStockTranslation(109, context.getString(R.string.connectivity_updating)); diff --git a/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCenter.java b/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCenter.java index ef685cfb9..95fea7d16 100644 --- a/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCenter.java +++ b/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCenter.java @@ -39,6 +39,7 @@ import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ConversationActivity; import org.thoughtcrime.securesms.ConversationListActivity; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.calls.CallActivity; import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference; @@ -49,16 +50,16 @@ import org.thoughtcrime.securesms.util.JsonUtils; import org.thoughtcrime.securesms.util.Pair; import org.thoughtcrime.securesms.util.Prefs; import org.thoughtcrime.securesms.util.Util; -import org.thoughtcrime.securesms.calls.CallActivity; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; -import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import chat.delta.rpc.RpcException; @@ -72,7 +73,7 @@ public class NotificationCenter { private static final long MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(2); // Map, contains the last lines of each chat for each account - private final HashMap>> inboxes = new HashMap<>(); + private final HashMap>> inboxes = new HashMap<>(); public NotificationCenter(Context context) { this.context = ApplicationContext.getInstance(context); @@ -563,47 +564,91 @@ public class NotificationCenter { @WorkerThread private void maybeAddNotification(int accountId, DcChat dcChat, int msgId, String shortLine, String tickerLine, boolean playInChatSound, boolean isMention) { + DcContext dcContext = ApplicationContext.getDcAccounts().getAccount(accountId); + int chatId = dcChat.getId(); + ChatData chatData = new ChatData(accountId, chatId); + isMention = isMention && dcContext.isMentionsEnabled(); - DcContext dcContext = context.getDcAccounts().getAccount(accountId); - int chatId = dcChat.getId(); - ChatData chatData = new ChatData(accountId, chatId); - isMention = isMention && dcContext.isMentionsEnabled(); + if (dcContext.isMuted() || (!isMention && dcChat.isMuted())) { + return; + } - if (dcContext.isMuted() || (!isMention && dcChat.isMuted())) { - return; + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !notificationManager.areNotificationsEnabled()) { + return; + } + + if (Util.equals(visibleChat, chatData)) { + if (playInChatSound && Prefs.isInChatNotifications(context)) { + InChatSounds.getInstance(context).playIncomingSound(); } + return; + } - NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !notificationManager.areNotificationsEnabled()) { - return; - } + NotificationPrivacyPreference privacy = Prefs.getNotificationPrivacy(context); + long now = System.currentTimeMillis(); + boolean signal = (now - lastAudibleNotification) > MIN_AUDIBLE_PERIOD_MILLIS; + if (signal) { + lastAudibleNotification = now; + } - if (Util.equals(visibleChat, chatData)) { - if (playInChatSound && Prefs.isInChatNotifications(context)) { - InChatSounds.getInstance(context).playIncomingSound(); + + // create a basic notification + // even without a name or message displayed, + // it makes sense to use separate notification channels and to open the respective chat directly - + // the user may eg. have chosen a different sound + String notificationChannel = getNotificationChannel(notificationManager, chatData, dcChat); + + LinkedHashMap messagesForInbox = null; + if (privacy.isDisplayContact() && privacy.isDisplayMessage()) { + synchronized (inboxes) { + HashMap> accountInbox = inboxes.get(accountId); + if (accountInbox == null) { + accountInbox = new HashMap<>(); + inboxes.put(accountId, accountInbox); } - return; + LinkedHashMap messages = accountInbox.get(chatId); + if (messages == null) { + messages = new LinkedHashMap<>(); + accountInbox.put(chatId, messages); + } + messages.put(msgId, shortLine); + messagesForInbox = new LinkedHashMap<>(messages); } + } + int cnt = dcContext.getFreshMsgCount(chatId); + buildAndShowChatNotification(accountId, chatId, msgId, dcContext, dcChat, + notificationChannel, shortLine, tickerLine, signal, messagesForInbox, cnt, true); + } + + @WorkerThread + private void buildAndShowChatNotification( + int accountId, + int chatId, + int msgId, + DcContext dcContext, + DcChat dcChat, + String notificationChannel, + String contentText, + String ticker, + boolean signal, + LinkedHashMap messagesForInbox, + int messageCount, + boolean includeSummary) { + try { + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); NotificationPrivacyPreference privacy = Prefs.getNotificationPrivacy(context); - long now = System.currentTimeMillis(); - boolean signal = (now - lastAudibleNotification) > MIN_AUDIBLE_PERIOD_MILLIS; - if (signal) { - lastAudibleNotification = now; - } + ChatData chatData = new ChatData(accountId, chatId); - // create a basic notification - // even without a name or message displayed, - // it makes sense to use separate notification channels and to open the respective chat directly - - // the user may eg. have chosen a different sound - String notificationChannel = getNotificationChannel(notificationManager, chatData, dcChat); + // Create basic notification NotificationCompat.Builder builder = new NotificationCompat.Builder(context, notificationChannel) .setSmallIcon(R.drawable.icon_notification) .setColor(context.getResources().getColor(R.color.def_accent)) .setPriority(Prefs.getNotificationPriority(context)) .setCategory(NotificationCompat.CATEGORY_MESSAGE) .setOnlyAlertOnce(!signal) - .setContentText(shortLine) + .setContentText(contentText) .setDeleteIntent(getMarkAsReadIntent(chatData, msgId, false)) .setContentIntent(getOpenChatIntent(chatData)); @@ -612,7 +657,7 @@ public class NotificationCenter { } String accountTag = dcContext.getConfig(CONFIG_PRIVATE_TAG); - if (accountTag.isEmpty() && context.getDcAccounts().getAll().length > 1) { + if (accountTag.isEmpty() && ApplicationContext.getDcAccounts().getAll().length > 1) { accountTag = dcContext.getName(); } @@ -623,9 +668,11 @@ public class NotificationCenter { } } - builder.setTicker(tickerLine); + if (ticker != null) { + builder.setTicker(ticker); + } - // set sound, vibrate, led for systems that do not have notification channels + // Set sound, vibrate, led for systems that do not have notification channels if (!notificationChannelsSupported()) { if (signal) { Uri sound = effectiveSound(chatData); @@ -639,20 +686,20 @@ public class NotificationCenter { } String ledColor = Prefs.getNotificationLedColor(context); if (!ledColor.equals("none")) { - builder.setLights(getLedArgb(ledColor),500, 2000); + builder.setLights(getLedArgb(ledColor), 500, 2000); } } - // set avatar + // Set avatar if (privacy.isDisplayContact()) { - Bitmap bitmap = getAvatar(dcChat); - if (bitmap != null) { - builder.setLargeIcon(bitmap); - } + Bitmap bitmap = getAvatar(dcChat); + if (bitmap != null) { + builder.setLargeIcon(bitmap); + } } - // add buttons that allow some actions without opening ArcaneChat. - // if privacy options are enabled, the buttons are not added. + // Add buttons that allow some actions without opening ArcaneChat. + // If privacy options are enabled, the buttons are not added. if (privacy.isDisplayContact() && privacy.isDisplayMessage()) { try { PendingIntent inNotificationReplyIntent = getRemoteReplyIntent(chatData, msgId); @@ -683,69 +730,90 @@ public class NotificationCenter { } catch(Exception e) { Log.w(TAG, e); } } - // create a tiny inbox (gets visible if the notification is expanded) - if (privacy.isDisplayContact() && privacy.isDisplayMessage()) { + // Create inbox style (gets visible if the notification is expanded) + if (privacy.isDisplayContact() && privacy.isDisplayMessage() && messagesForInbox != null) { try { NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); - synchronized (inboxes) { - HashMap> accountInbox = inboxes.get(accountId); - if (accountInbox == null) { - accountInbox = new HashMap<>(); - inboxes.put(accountId, accountInbox); - } - ArrayList lines = accountInbox.get(chatId); - if (lines == null) { - lines = new ArrayList<>(); - accountInbox.put(chatId, lines); - } - lines.add(shortLine); - - for (int l = 0; l < lines.size(); l++) { - inboxStyle.addLine(lines.get(l)); - } + for (String line : messagesForInbox.values()) { + inboxStyle.addLine(line); } builder.setStyle(inboxStyle); } catch(Exception e) { Log.w(TAG, e); } } - // messages count, some os make some use of that - // - do not use setSubText() as this is displayed together with setContentInfo() eg. on Lollipop - // - setNumber() may overwrite setContentInfo(), should be called last - // weird stuff. - int cnt = dcContext.getFreshMsgCount(chatId); - builder.setContentInfo(String.valueOf(cnt)); - builder.setNumber(cnt); + // Messages count + builder.setContentInfo(String.valueOf(messageCount)); + builder.setNumber(messageCount); - // add notification, we use one notification per chat, - // esp. older android are not that great at grouping + // Show notification + // try..catch potentially needed for very specific devices try { - notificationManager.notify(String.valueOf(accountId), ID_MSG_OFFSET + chatId, builder.build()); + notificationManager.notify(String.valueOf(accountId), ID_MSG_OFFSET + chatId, builder.build()); } catch (Exception e) { - Log.e(TAG, "cannot add notification", e); + Log.e(TAG, "cannot add notification", e); } - // group notifications together in a summary, this is possible since SDK 24 (Android 7) - // https://developer.android.com/training/notify-user/group.html - // in theory, this won't be needed due to setGroup(), however, in practise, it is needed up to at least Android 10. - if (Build.VERSION.SDK_INT >= 24) { + // Group notifications in a summary (Android 7+) + if (includeSummary && Build.VERSION.SDK_INT >= 24) { try { - NotificationCompat.Builder summary = new NotificationCompat.Builder(context, notificationChannel) - .setGroup(GRP_MSG + "." + accountId) - .setGroupSummary(true) - .setSmallIcon(R.drawable.icon_notification) - .setColor(context.getResources().getColor(R.color.def_accent, null)) - .setCategory(NotificationCompat.CATEGORY_MESSAGE) - .setContentTitle("ArcaneChat") // content title would only be used on SDK <24 - .setContentText("New messages") // content text would only be used on SDK <24 - .setContentIntent(getOpenChatlistIntent(accountId)); - if (privacy.isDisplayContact() && !TextUtils.isEmpty(accountTag)) { - summary.setSubText(accountTag); - } - notificationManager.notify(String.valueOf(accountId), ID_MSG_SUMMARY, summary.build()); + NotificationCompat.Builder summary = new NotificationCompat.Builder(context, notificationChannel) + .setGroup(GRP_MSG + "." + accountId) + .setGroupSummary(true) + .setSmallIcon(R.drawable.icon_notification) + .setColor(context.getResources().getColor(R.color.def_accent, null)) + .setCategory(NotificationCompat.CATEGORY_MESSAGE) + .setContentTitle("ArcaneChat") + .setContentText("New messages") + .setContentIntent(getOpenChatlistIntent(accountId)); + if (privacy.isDisplayContact() && !TextUtils.isEmpty(accountTag)) { + summary.setSubText(accountTag); + } + notificationManager.notify(String.valueOf(accountId), ID_MSG_SUMMARY, summary.build()); } catch (Exception e) { - Log.e(TAG, "cannot add notification summary", e); + Log.e(TAG, "cannot add notification summary", e); } } + } catch (Exception e) { + Log.e(TAG, "cannot show notification", e); + } + } + + @WorkerThread + private void rebuildNotification(int accountId, int chatId, LinkedHashMap messages) { + try { + DcContext dcContext = ApplicationContext.getDcAccounts().getAccount(accountId); + DcChat dcChat = dcContext.getChat(chatId); + + if (dcContext.isMuted() || dcChat.isMuted()) { + return; + } + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !notificationManager.areNotificationsEnabled()) { + return; + } + + // Get the latest message ID (last entry in LinkedHashMap) + Integer latestMsgId = null; + String lastLine = null; + for (Map.Entry entry : messages.entrySet()) { + latestMsgId = entry.getKey(); + lastLine = entry.getValue(); + } + if (latestMsgId == null || lastLine == null) { + return; + } + + ChatData chatData = new ChatData(accountId, chatId); + String notificationChannel = getNotificationChannel(notificationManager, chatData, dcChat); + + int cnt = dcContext.getFreshMsgCount(chatId); + buildAndShowChatNotification(accountId, chatId, latestMsgId, dcContext, dcChat, + notificationChannel, lastLine, null, false, messages, cnt, false); + + } catch (Exception e) { + Log.e(TAG, "cannot rebuild notification", e); + } } public Bitmap getAvatar(DcChat dcChat) { @@ -782,10 +850,48 @@ public class NotificationCenter { } catch (Exception e) { Log.w(TAG, e); } } + public void removeNotification(int accountId, int chatId, int msgId) { + boolean shouldCancelNotification = false; + boolean removeSummary = false; + LinkedHashMap remainingMessages = null; + + synchronized (inboxes) { + HashMap> accountInbox = inboxes.get(accountId); + if (accountInbox != null) { + LinkedHashMap messages = accountInbox.get(chatId); + if (messages != null) { + messages.remove(msgId); + + if (messages.isEmpty()) { + // No more messages for this chat + accountInbox.remove(chatId); + shouldCancelNotification = true; + removeSummary = accountInbox.isEmpty(); + } else { + // Keep a copy of remaining messages for rebuilding + remainingMessages = new LinkedHashMap<>(messages); + } + } + } + } + + if (shouldCancelNotification) { + // Cancel the notification entirely + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + String tag = String.valueOf(accountId); + notificationManager.cancel(tag, ID_MSG_OFFSET + chatId); + if (removeSummary) { + notificationManager.cancel(tag, ID_MSG_SUMMARY); + } + } else if (remainingMessages != null && !remainingMessages.isEmpty()) { + rebuildNotification(accountId, chatId, remainingMessages); + } + } + public void removeNotifications(int accountId, int chatId) { boolean removeSummary; synchronized (inboxes) { - HashMap> accountInbox = inboxes.get(accountId); + HashMap> accountInbox = inboxes.get(accountId); if (accountInbox == null) { accountInbox = new HashMap<>(); } @@ -809,11 +915,11 @@ public class NotificationCenter { NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); String tag = String.valueOf(accountId); synchronized (inboxes) { - HashMap> accountInbox = inboxes.get(accountId); + HashMap> accountInbox = inboxes.get(accountId); notificationManager.cancel(tag, ID_MSG_SUMMARY); if (accountInbox != null) { for (Integer chatId : accountInbox.keySet()) { - notificationManager.cancel(tag, chatId); + notificationManager.cancel(tag, ID_MSG_OFFSET + chatId); } accountInbox.clear(); } diff --git a/src/main/java/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java b/src/main/java/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java index 7c8796a71..f6bbe11f0 100644 --- a/src/main/java/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java @@ -1,18 +1,11 @@ package org.thoughtcrime.securesms.preferences; - import android.os.Bundle; import android.view.View; -import androidx.annotation.NonNull; -import androidx.fragment.app.DialogFragment; -import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; -import org.thoughtcrime.securesms.components.CustomDefaultPreference; - public abstract class CorrectedPreferenceFragment extends PreferenceFragmentCompat { - @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -25,22 +18,4 @@ public abstract class CorrectedPreferenceFragment extends PreferenceFragmentComp View lv = getView().findViewById(android.R.id.list); if (lv != null) lv.setPadding(0, 0, 0, 0); } - - @Override - public void onDisplayPreferenceDialog(@NonNull Preference preference) { - DialogFragment dialogFragment = null; - - if (preference instanceof CustomDefaultPreference) { - dialogFragment = CustomDefaultPreference.CustomDefaultPreferenceDialogFragmentCompat.newInstance(preference.getKey()); - } - - if (dialogFragment != null) { - dialogFragment.setTargetFragment(this, 0); - dialogFragment.show(getFragmentManager(), "android.support.v7.preference.PreferenceFragment.DIALOG"); - } else { - super.onDisplayPreferenceDialog(preference); - } - } - - } diff --git a/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java b/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java index 0a99bf7e1..eb99f6367 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java +++ b/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java @@ -121,11 +121,11 @@ public class DateUtils extends android.text.format.DateUtils { public static String getFormattedCallDuration(Context c, int seconds) { if (seconds < 60) { - return c.getResources().getQuantityString(R.plurals.n_seconds_ext, seconds, seconds); + return c.getResources().getString(R.string.call_duration_less_than_a_minute); } int mins = seconds / 60; - return c.getResources().getQuantityString(R.plurals.n_minutes_ext, mins, mins); + return c.getResources().getQuantityString(R.plurals.call_duration_minutes, mins, mins); } public static String getFormattedTimespan(Context c, int timestamp) { diff --git a/src/main/res/drawable/ic_sticker_24.xml b/src/main/res/drawable/ic_sticker_24.xml new file mode 100644 index 000000000..102dedfd5 --- /dev/null +++ b/src/main/res/drawable/ic_sticker_24.xml @@ -0,0 +1,3 @@ + + + diff --git a/src/main/res/layout-land/conversation_activity_emojidrawer_stub.xml b/src/main/res/layout-land/conversation_activity_emojidrawer_stub.xml deleted file mode 100644 index 9417360bf..000000000 --- a/src/main/res/layout-land/conversation_activity_emojidrawer_stub.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - diff --git a/src/main/res/layout/call_item_view.xml b/src/main/res/layout/call_item_view.xml index 32aea6633..84eabaea9 100644 --- a/src/main/res/layout/call_item_view.xml +++ b/src/main/res/layout/call_item_view.xml @@ -9,10 +9,17 @@ android:gravity="center_vertical" android:focusable="true"> + + - + - + diff --git a/src/main/res/layout/conversation_activity_emojidrawer_stub.xml b/src/main/res/layout/conversation_activity_emojidrawer_stub.xml index 742adefaf..02514ac2c 100644 --- a/src/main/res/layout/conversation_activity_emojidrawer_stub.xml +++ b/src/main/res/layout/conversation_activity_emojidrawer_stub.xml @@ -1,19 +1,69 @@ - - + + + + + + + + + + + + + + + app:tabMode="fixed" + app:tabGravity="center" + app:tabIndicatorGravity="top" + app:tabIconTint="?attr/colorControlNormal" + app:tabPaddingStart="16dp" + app:tabPaddingEnd="16dp" + app:tabPaddingTop="10dp" + app:tabPaddingBottom="10dp" + /> diff --git a/src/main/res/layout/custom_default_preference_dialog.xml b/src/main/res/layout/custom_default_preference_dialog.xml deleted file mode 100644 index e9497ee6f..000000000 --- a/src/main/res/layout/custom_default_preference_dialog.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/src/main/res/layout/pref_dialog_with_hint.xml b/src/main/res/layout/pref_dialog_with_hint.xml new file mode 100644 index 000000000..f8038e89e --- /dev/null +++ b/src/main/res/layout/pref_dialog_with_hint.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/src/main/res/layout/sticker_picker_item.xml b/src/main/res/layout/sticker_picker_item.xml new file mode 100644 index 000000000..6f02a0d19 --- /dev/null +++ b/src/main/res/layout/sticker_picker_item.xml @@ -0,0 +1,7 @@ + + diff --git a/src/main/res/values-land/integers.xml b/src/main/res/values-land/integers.xml new file mode 100644 index 000000000..b3a0b3815 --- /dev/null +++ b/src/main/res/values-land/integers.xml @@ -0,0 +1,4 @@ + + + 12 + diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml index 241eadb24..5a54f2f74 100644 --- a/src/main/res/values/attrs.xml +++ b/src/main/res/values/attrs.xml @@ -84,13 +84,8 @@ - - - - - diff --git a/src/main/res/values/integers.xml b/src/main/res/values/integers.xml index bb4c483c9..d485069c5 100644 --- a/src/main/res/values/integers.xml +++ b/src/main/res/values/integers.xml @@ -1,4 +1,5 @@ 300 - \ No newline at end of file + 9 + diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index b7be62fba..263cba76a 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -118,16 +118,13 @@ last seen %1$s last seen: Unknown - - - %d second - %d seconds - - - - %d minute - %d minutes + + + %d minute duration + %d minutes duration + + Less than 1 minute %d min @@ -180,6 +177,9 @@ Add to Sticker Collection To add stickers, tap "Open Sticker Folder", create a subfolder for your sticker pack, and drag image and sticker files there Open Sticker Folder + Add this sticker to your collection? + Delete this sticker? + Tap a sticker to add it here. Images Audio @@ -259,10 +259,13 @@ Channels New Channel - - Add Subscribers Channel Name + + + %d view + %d views + Email New Email @@ -390,19 +393,13 @@ Answer Decline - - Outgoing call Outgoing audio call Outgoing video call - - Incoming call Incoming audio call Incoming video call Declined call Canceled call Missed call - - %1$s, %2$s @@ -588,8 +585,6 @@ Incoming Messages Outgoing Messages - - Storage on %1$s Connectivity Not connected @@ -718,8 +713,6 @@ - Using custom: %s - Using default: %s Your Profile Profile Image Blocked Contacts @@ -733,6 +726,7 @@ Enter Key Sends Pressing the Enter key will send text messages Outgoing Media Quality + To send original quality, attach media as a \"File\". This uses significantly more data for you and your recipients. Balanced Worse quality, small size Vibrate @@ -743,6 +737,10 @@ Notifications Mentions In muted groups, notify messages directed to you, like replies or reactions + + Calls + + Show call screen on incoming calls Show Priority Enable system notifications for new messages @@ -752,8 +750,6 @@ Sound Silent Privacy - - Chats and Media System default Light @@ -818,8 +814,6 @@ %1$s message Download maximum available until %1$s - - Select Profile Select Profile Image Select your new profile image Delete Profile Image @@ -925,10 +919,6 @@ You set disappearing messages timer to %1$s seconds Disappearing messages timer set to %1$s seconds by %2$s. - - You set disappearing messages timer to 1 minute. - - Disappearing messages timer set to 1 minute by %1$s. You set disappearing messages timer to 1 hour. Disappearing messages timer set to 1 hour by %1$s. @@ -1025,8 +1015,6 @@ Introduced by %1$s Introduced by me Introduced - - To guarantee end-to-end-encryption, you can only add contacts with a green checkmark to this group.\n\nYou may meet contacts in person and scan their QR Code to introduce them. Select chat to send the message to %1$s already has a draft message, do you want to replace it? diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml index 447962128..a45a922ee 100644 --- a/src/main/res/values/themes.xml +++ b/src/main/res/values/themes.xml @@ -13,7 +13,6 @@ @color/white @color/gray70 - @color/gray50 @color/gray70 @@ -29,7 +28,6 @@ @color/black @color/white - @color/gray10 @color/white @@ -299,7 +297,6 @@ @color/white @color/gray70 - @color/gray50 @color/gray70 @@ -329,7 +326,6 @@ @color/black @color/white - @color/gray10 @color/white @@ -365,7 +361,6 @@ @color/white @color/gray70 - @color/gray50 @color/gray70 @@ -395,7 +390,6 @@ @color/black @color/white - @color/gray10 @color/white @@ -432,7 +426,6 @@ @color/white @color/gray70 - @color/gray50 @color/gray70 @@ -462,7 +455,6 @@ @color/black @color/white - @color/gray10 @color/white @@ -498,7 +490,6 @@ @color/white @color/gray70 - @color/gray50 @color/gray70 @@ -528,7 +519,6 @@ @color/black @color/white - @color/gray10 @color/white @@ -564,7 +554,6 @@ @color/white @color/gray70 - @color/gray50 @color/gray70 @@ -594,7 +583,6 @@ @color/black @color/white - @color/gray10 @color/white @@ -630,7 +618,6 @@ @color/white @color/gray70 - @color/gray50 @color/gray70 @@ -660,7 +647,6 @@ @color/black @color/white - @color/gray10 @color/white diff --git a/src/main/res/xml/preferences_chats.xml b/src/main/res/xml/preferences_chats.xml index f15d24889..2e167f419 100644 --- a/src/main/res/xml/preferences_chats.xml +++ b/src/main/res/xml/preferences_chats.xml @@ -11,7 +11,7 @@