diff --git a/.github/workflows/preview-apk.yml b/.github/workflows/preview-apk.yml index dc2599659..a5572ed81 100644 --- a/.github/workflows/preview-apk.yml +++ b/.github/workflows/preview-apk.yml @@ -43,10 +43,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 assembleGplayDebug + run: ./gradlew --no-daemon -PABI_FILTER=arm64-v8a assembleGplayDebug - name: Upload APK uses: actions/upload-artifact@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 54abfb28e..40a4b6eb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,11 @@ ## Unreleased * Allow to set chat description +* Unified date display in call bubbles +* Explain at "Settings / Chats / Outgoing Media Quality" how to send original quality * 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 dd80e8fd7..ef993ccee 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/ConversationItem.java b/src/main/java/org/thoughtcrime/securesms/ConversationItem.java index 6316afb78..59e7fc5e9 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationItem.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationItem.java @@ -803,8 +803,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; } 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/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 e8d7ef0bf..6dc30f078 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 323862231..67f69c877 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.delta_primary)) .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 Delta Chat. - // if privacy options are enabled, the buttons are not added. + // Add buttons that allow some actions without opening Delta Chat. + // 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.delta_primary, null)) - .setCategory(NotificationCompat.CATEGORY_MESSAGE) - .setContentTitle("Delta Chat") // 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.delta_primary, null)) + .setCategory(NotificationCompat.CATEGORY_MESSAGE) + .setContentTitle("Delta Chat") + .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/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/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/values/attrs.xml b/src/main/res/values/attrs.xml index dcc89a380..19ba299cf 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/strings.xml b/src/main/res/values/strings.xml index 161246ff6..ef9ded293 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 @@ -262,10 +259,13 @@ Channels New Channel - - Add Subscribers Channel Name + + + %d view + %d views + Email New Email @@ -393,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 @@ -591,8 +585,6 @@ Incoming Messages Outgoing Messages - - Storage on %1$s Connectivity Not connected @@ -721,8 +713,6 @@ - Using custom: %s - Using default: %s Your Profile Profile Image Blocked Contacts @@ -736,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 @@ -746,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 @@ -755,8 +750,6 @@ Sound Silent Privacy - - Chats and Media System default Light @@ -821,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 @@ -928,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. @@ -1028,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 2b049601a..45377d246 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 diff --git a/src/main/res/xml/preferences_chats.xml b/src/main/res/xml/preferences_chats.xml index da2ba740d..35a73f505 100644 --- a/src/main/res/xml/preferences_chats.xml +++ b/src/main/res/xml/preferences_chats.xml @@ -21,7 +21,7 @@