Threema 11 mesiacov pred
rodič
commit
d343b79dbf
100 zmenil súbory, kde vykonal 2634 pridanie a 2230 odobranie
  1. 2 2
      app/build.gradle
  2. 2 2
      app/src/androidTest/java/ch/threema/app/groupmanagement/IncomingGroupSetupTest.kt
  3. 2 2
      app/src/androidTest/java/ch/threema/app/processors/MessageProcessorProvider.kt
  4. 1 1
      app/src/androidTest/java/ch/threema/app/utils/TextUtilTest.java
  5. 2 3
      app/src/libre/play/release-notes/de/default.txt
  6. 2 3
      app/src/libre/play/release-notes/en-US/default.txt
  7. 1 1
      app/src/main/AndroidManifest.xml
  8. 4 4
      app/src/main/java/ch/threema/app/activities/BlockedContactsActivity.java
  9. 7 8
      app/src/main/java/ch/threema/app/activities/ContactDetailActivity.java
  10. 4 4
      app/src/main/java/ch/threema/app/activities/GroupDetailActivity.java
  11. 31 3
      app/src/main/java/ch/threema/app/activities/HomeActivity.java
  12. 241 258
      app/src/main/java/ch/threema/app/activities/IdentityListActivity.java
  13. 9 9
      app/src/main/java/ch/threema/app/activities/ImagePaintActivity.java
  14. 1 1
      app/src/main/java/ch/threema/app/activities/MessageDetailsActivity.kt
  15. 13 8
      app/src/main/java/ch/threema/app/activities/MessageDetailsViewModel.kt
  16. 5 1
      app/src/main/java/ch/threema/app/activities/StarredMessagesActivity.kt
  17. 4 3
      app/src/main/java/ch/threema/app/adapters/ComposeMessageAdapter.java
  18. 3 4
      app/src/main/java/ch/threema/app/adapters/ContactDetailAdapter.java
  19. 4 4
      app/src/main/java/ch/threema/app/adapters/ContactListAdapter.java
  20. 10 4
      app/src/main/java/ch/threema/app/adapters/MessageListViewHolder.kt
  21. 4 4
      app/src/main/java/ch/threema/app/adapters/UserListAdapter.java
  22. 13 2
      app/src/main/java/ch/threema/app/adapters/decorators/AudioChatAdapterDecorator.java
  23. 1 1
      app/src/main/java/ch/threema/app/adapters/decorators/BallotChatAdapterDecorator.java
  24. 563 520
      app/src/main/java/ch/threema/app/adapters/decorators/ChatAdapterDecorator.java
  25. 14 0
      app/src/main/java/ch/threema/app/adapters/decorators/DateSeparatorChatAdapterDecorator.java
  26. 1 1
      app/src/main/java/ch/threema/app/adapters/decorators/FileChatAdapterDecorator.java
  27. 1 1
      app/src/main/java/ch/threema/app/adapters/decorators/LocationChatAdapterDecorator.java
  28. 1 1
      app/src/main/java/ch/threema/app/adapters/decorators/VoipStatusDataChatAdapterDecorator.java
  29. 6 0
      app/src/main/java/ch/threema/app/compose/common/interop/InteropEmojiConversationTextView.kt
  30. 3 1
      app/src/main/java/ch/threema/app/compose/message/DeliveryIcon.kt
  31. 35 11
      app/src/main/java/ch/threema/app/compose/message/MessageBubble.kt
  32. 7 1
      app/src/main/java/ch/threema/app/compose/message/MessageStateIndicator.kt
  33. 18 15
      app/src/main/java/ch/threema/app/fragments/ComposeMessageFragment.java
  34. 5 5
      app/src/main/java/ch/threema/app/fragments/ContactsSectionFragment.java
  35. 2 2
      app/src/main/java/ch/threema/app/fragments/MemberListFragment.java
  36. 2 2
      app/src/main/java/ch/threema/app/fragments/RecipientListFragment.java
  37. 1 1
      app/src/main/java/ch/threema/app/fragments/UserListFragment.java
  38. 1 1
      app/src/main/java/ch/threema/app/fragments/UserMemberListFragment.java
  39. 1 1
      app/src/main/java/ch/threema/app/fragments/WorkUserListFragment.java
  40. 1 1
      app/src/main/java/ch/threema/app/fragments/WorkUserMemberListFragment.java
  41. 5 1
      app/src/main/java/ch/threema/app/globalsearch/GlobalSearchActivity.kt
  42. 453 403
      app/src/main/java/ch/threema/app/globalsearch/GlobalSearchAdapter.java
  43. 13 13
      app/src/main/java/ch/threema/app/managers/ServiceManager.java
  44. 4 4
      app/src/main/java/ch/threema/app/mediaattacher/MediaSelectionBaseActivity.java
  45. 5 5
      app/src/main/java/ch/threema/app/messagereceiver/ContactMessageReceiver.java
  46. 1 1
      app/src/main/java/ch/threema/app/multidevice/linking/DeviceJoinDataCollector.kt
  47. 5 5
      app/src/main/java/ch/threema/app/preference/SettingsPrivacyFragment.kt
  48. 2 2
      app/src/main/java/ch/threema/app/processors/IncomingMessageProcessorImpl.kt
  49. 3 3
      app/src/main/java/ch/threema/app/processors/groupcontrol/IncomingGroupSetupTask.kt
  50. 26 29
      app/src/main/java/ch/threema/app/routines/SynchronizeContactsRoutine.java
  51. 4 4
      app/src/main/java/ch/threema/app/services/ContactServiceImpl.java
  52. 4 4
      app/src/main/java/ch/threema/app/services/ConversationServiceImpl.java
  53. 4 4
      app/src/main/java/ch/threema/app/services/MessageServiceImpl.java
  54. 50 12
      app/src/main/java/ch/threema/app/services/RingtoneService.java
  55. 44 6
      app/src/main/java/ch/threema/app/services/RingtoneServiceImpl.java
  56. 16 21
      app/src/main/java/ch/threema/app/services/SynchronizeContactsServiceImpl.java
  57. 2 2
      app/src/main/java/ch/threema/app/tasks/OutgoingCspGroupControlMessageTask.kt
  58. 3 3
      app/src/main/java/ch/threema/app/tasks/OutgoingCspMessageTask.kt
  59. 2 2
      app/src/main/java/ch/threema/app/tasks/OutgoingGroupSyncRequestTask.kt
  60. 5 5
      app/src/main/java/ch/threema/app/threemasafe/ThreemaSafeServiceImpl.java
  61. 1 1
      app/src/main/java/ch/threema/app/ui/AudioProgressBarView.kt
  62. 5 7
      app/src/main/java/ch/threema/app/ui/AvatarEditView.java
  63. 281 188
      app/src/main/java/ch/threema/app/ui/ControllerView.java
  64. 6 1
      app/src/main/java/ch/threema/app/ui/EmptyView.java
  65. 1 1
      app/src/main/java/ch/threema/app/ui/OpenBallotNoticeView.java
  66. 1 1
      app/src/main/java/ch/threema/app/ui/listitemholder/ComposeMessageHolder.java
  67. 121 99
      app/src/main/java/ch/threema/app/utils/ColorUtil.java
  68. 9 20
      app/src/main/java/ch/threema/app/utils/ConfigUtils.java
  69. 3 3
      app/src/main/java/ch/threema/app/utils/ContactUtil.java
  70. 10 10
      app/src/main/java/ch/threema/app/utils/OutgoingCspMessageUtils.kt
  71. 21 4
      app/src/main/java/ch/threema/app/utils/StateBitmapUtil.java
  72. 11 8
      app/src/main/java/ch/threema/app/voip/services/VoipStateService.java
  73. 1 1
      app/src/main/java/ch/threema/app/voip/util/VoipUtil.java
  74. 1 1
      app/src/main/java/ch/threema/app/webclient/activities/SessionsActivity.java
  75. 1 1
      app/src/main/java/ch/threema/app/webclient/converter/Contact.java
  76. 2 2
      app/src/main/java/ch/threema/app/webclient/converter/Converter.java
  77. 3 3
      app/src/main/java/ch/threema/app/webclient/services/ServicesContainer.java
  78. 2 2
      app/src/main/java/ch/threema/app/webclient/services/instance/SessionInstanceServiceImpl.java
  79. 2 2
      app/src/main/java/ch/threema/app/webclient/services/instance/message/receiver/FileMessageCreateHandler.java
  80. 4 4
      app/src/main/java/ch/threema/app/webclient/services/instance/message/receiver/MessageCreateHandler.java
  81. 2 2
      app/src/main/java/ch/threema/app/webclient/services/instance/message/receiver/TextMessageCreateHandler.java
  82. 22 0
      app/src/main/java/ch/threema/storage/models/AbstractMessageModel.java
  83. 1 1
      app/src/main/java/ch/threema/storage/models/ContactModel.java
  84. 0 0
      app/src/main/res/color/bubble_receive_text_colorstatelist.xml
  85. 5 0
      app/src/main/res/color/bubble_send_text_colorstatelist.xml
  86. 7 7
      app/src/main/res/drawable/ic_circle_send.xml
  87. 7 7
      app/src/main/res/drawable/ic_circle_send_disabled.xml
  88. 1 1
      app/src/main/res/drawable/ic_collections.xml
  89. 0 9
      app/src/main/res/drawable/ic_places_hearing_aids.xml
  90. 5 5
      app/src/main/res/drawable/ic_search_outline.xml
  91. 76 80
      app/src/main/res/layout/activity_contact_detail.xml
  92. 0 1
      app/src/main/res/layout/activity_group_detail.xml
  93. 58 59
      app/src/main/res/layout/activity_home.xml
  94. 58 58
      app/src/main/res/layout/activity_media_viewer.xml
  95. 7 8
      app/src/main/res/layout/content_home.xml
  96. 44 44
      app/src/main/res/layout/conversation_bubble_footer_groupack_recv.xml
  97. 67 59
      app/src/main/res/layout/conversation_bubble_footer_recv.xml
  98. 18 18
      app/src/main/res/layout/conversation_bubble_footer_send.xml
  99. 23 21
      app/src/main/res/layout/conversation_bubble_header.xml
  100. 68 66
      app/src/main/res/layout/conversation_list_item_audio.xml

+ 2 - 2
app/build.gradle

@@ -18,14 +18,14 @@ if (getGradle().getStartParameter().getTaskRequests().toString().contains("Hms")
 // version codes
 
 // Only use the scheme "<major>.<minor>.<patch>" for the app_version
-def app_version = "5.6.0"
+def app_version = "5.6.1"
 
 // beta_suffix with leading dash (e.g. `-beta1`)
 // should be one of (alpha|beta|rc) and an increasing number or empty for a regular release.
 // Note: in nightly builds this will be overwritten with a nightly version "-n12345"
 def beta_suffix = ""
 
-def defaultVersionCode = 1012
+def defaultVersionCode = 1013
 
 /**
  * Return the git hash, if git is installed.

+ 2 - 2
app/src/androidTest/java/ch/threema/app/groupmanagement/IncomingGroupSetupTest.kt

@@ -145,8 +145,8 @@ class IncomingGroupSetupTest : GroupConversationListTest<GroupSetupMessage>() {
         // Assert initial group conversations
         assertGroupConversations(scenario, initialGroups)
 
-        serviceManager.blackListService.add(contactA.identity)
-        serviceManager.blackListService.add(contactB.identity)
+        serviceManager.blockedContactsService.add(contactA.identity)
+        serviceManager.blockedContactsService.add(contactB.identity)
 
         val setupTracker = GroupSetupTracker(
             newAGroup,

+ 2 - 2
app/src/androidTest/java/ch/threema/app/processors/MessageProcessorProvider.kt

@@ -380,7 +380,7 @@ open class MessageProcessorProvider {
         )
 
         // Unblock contacts
-        serviceManager.blackListService.removeAll()
+        serviceManager.blockedContactsService.removeAll()
     }
 
     private fun setTaskManager(taskManager: TaskManager) {
@@ -487,7 +487,7 @@ open class MessageProcessorProvider {
                 it.contactService,
                 it.contactStore,
                 it.identityStore,
-                it.blackListService,
+                it.blockedContactsService,
                 it.preferenceService,
                 it
             )

+ 1 - 1
app/src/androidTest/java/ch/threema/app/utils/TextUtilTest.java

@@ -62,7 +62,7 @@ public class TextUtilTest {
 	}
 
 	@Test
-	public void testCheckBadPasswordBlacklisted() {
+	public void testCheckBadPasswordWarnList() {
 		final Context context = ThreemaApplication.getAppContext();
 		assertTrue(TextUtil.checkBadPassword(context, "1Rainbow"));
 		assertTrue(TextUtil.checkBadPassword(context, "apples123"));

+ 2 - 3
app/src/libre/play/release-notes/de/default.txt

@@ -1,3 +1,2 @@
-* Überarbeitung der Benachrichtigungskanäle: Individuelle Benachrichtigungseinstellungen müssen erneut vorgenommen werden
-* Medien lassen sich neu vor dem Weiterleiten bearbeiten
-* Bei bearbeiteten Nachrichten ist nun der Bearbeitungsverlauf einsehbar
+* Verbesserung bei der Verwendung von «dynamischen Farben»
+* Behebung eines Fehlers, dass auch bei aktivem «Bitte nicht stören» eine Benachrichtigung mit Vibration ausgelöst wurde

+ 2 - 3
app/src/libre/play/release-notes/en-US/default.txt

@@ -1,3 +1,2 @@
-* Refactoring of notification channels: Individual notification settings need to be set again
-* Media can now be edited before forwarding
-* The edit history can now be viewed for edited messages
+* Improvements when “dynamic colors” are used
+* Fix of a bug, that could cause a vibrating notification even if “Do not disturb” is active

+ 1 - 1
app/src/main/AndroidManifest.xml

@@ -498,7 +498,7 @@
 			android:theme="@style/Theme.Threema.WithToolbar"
 			android:configChanges="uiMode" />
 		<activity
-			android:name=".activities.BlackListActivity"
+			android:name=".activities.BlockedContactsActivity"
 			android:theme="@style/Theme.Threema.WithToolbar"
 			android:configChanges="uiMode" />
 		<activity

+ 4 - 4
app/src/main/java/ch/threema/app/activities/BlackListActivity.java → app/src/main/java/ch/threema/app/activities/BlockedContactsActivity.java

@@ -25,24 +25,24 @@ import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.services.IdListService;
 
-public class BlackListActivity extends IdentityListActivity {
+public class BlockedContactsActivity extends IdentityListActivity {
 	private IdListService listService;
 
 	@Override
 	protected IdListService getIdentityListService() {
 		if(this.listService == null) {
-			this.listService = ThreemaApplication.getServiceManager().getBlackListService();
+			this.listService = ThreemaApplication.getServiceManager().getBlockedContactsService();
 		}
 		return this.listService;
 	}
 
 	@Override
 	protected String getBlankListText() {
-		return this.getString(R.string.prefs_sum_black_list);
+		return this.getString(R.string.prefs_sum_blocked_contacts);
 	}
 
 	@Override
 	protected String getTitleText() {
-		return this.getString(R.string.prefs_title_black_list);
+		return this.getString(R.string.prefs_title_blocked_contacts);
 	}
 }

+ 7 - 8
app/src/main/java/ch/threema/app/activities/ContactDetailActivity.java

@@ -73,7 +73,6 @@ import ch.threema.app.listeners.ContactSettingsListener;
 import ch.threema.app.listeners.GroupListener;
 import ch.threema.app.managers.ListenerManager;
 import ch.threema.app.services.ContactService;
-import ch.threema.app.services.ConversationService;
 import ch.threema.app.services.DeadlineListService;
 import ch.threema.app.services.GroupService;
 import ch.threema.app.services.IdListService;
@@ -131,7 +130,7 @@ public class ContactDetailActivity extends ThreemaToolbarActivity
 	// Services
 	private ContactService contactService;
 	private GroupService groupService;
-	private IdListService blackListIdentityService, profilePicRecipientsService;
+	private IdListService blockedContactsService, profilePicRecipientsService;
 	private DeadlineListService hiddenChatsListService;
 	private VoipStateService voipStateService;
 
@@ -303,7 +302,7 @@ public class ContactDetailActivity extends ThreemaToolbarActivity
 		try {
 			this.contactService = serviceManager.getContactService();
 			modelRepositories = serviceManager.getModelRepositories();
-			this.blackListIdentityService = serviceManager.getBlackListService();
+			this.blockedContactsService = serviceManager.getBlockedContactsService();
 			this.profilePicRecipientsService = serviceManager.getProfilePicRecipientsService();
 			this.groupService = serviceManager.getGroupService();
 			this.hiddenChatsListService = serviceManager.getHiddenChatsListService();
@@ -670,7 +669,7 @@ public class ContactDetailActivity extends ThreemaToolbarActivity
 	private void updateVoipCallMenuItem(final Boolean newState) {
 		if (callItem != null) {
 			if (
-				ContactUtil.canReceiveVoipMessages(contact, blackListIdentityService)
+				ContactUtil.canReceiveVoipMessages(contact, blockedContactsService)
 					&& ConfigUtils.isCallsEnabled()) {
 				logger.debug("updateVoipMenu newState " + newState);
 
@@ -702,7 +701,7 @@ public class ContactDetailActivity extends ThreemaToolbarActivity
 		} else if (id == R.id.menu_threema_call) {
 			VoipUtil.initiateCall(this, contact, false, null);
 		} else if (id == R.id.action_block_contact) {
-			if (this.blackListIdentityService != null && this.blackListIdentityService.has(this.identity)) {
+			if (this.blockedContactsService != null && this.blockedContactsService.has(this.identity)) {
 				blockContact();
 			} else {
 				GenericAlertDialog.newInstance(R.string.block_contact, R.string.really_block_contact, R.string.yes, R.string.no).show(getSupportFragmentManager(), DIALOG_TAG_CONFIRM_BLOCK);
@@ -735,14 +734,14 @@ public class ContactDetailActivity extends ThreemaToolbarActivity
 	}
 
 	private void blockContact() {
-		if (this.blackListIdentityService != null) {
-			this.blackListIdentityService.toggle(this, this.contact);
+		if (this.blockedContactsService != null) {
+			this.blockedContactsService.toggle(this, this.contact);
 		}
 	}
 
 	private void updateBlockMenu() {
 		if (this.blockMenuItem != null) {
-			if (blackListIdentityService != null && blackListIdentityService.has(this.identity)) {
+			if (blockedContactsService != null && blockedContactsService.has(this.identity)) {
 				blockMenuItem.setTitle(R.string.unblock_contact);
 			} else {
 				blockMenuItem.setTitle(R.string.block_contact);

+ 4 - 4
app/src/main/java/ch/threema/app/activities/GroupDetailActivity.java

@@ -150,7 +150,7 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 
 	private GroupInviteService groupInviteService;
 	private DeviceService deviceService;
-	private IdListService blackListIdentityService;
+	private IdListService blockedContactsService;
 	private GroupCallManager groupCallManager;
 
 	private GroupModel groupModel;
@@ -324,7 +324,7 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 		// services
 		try {
 			this.deviceService = serviceManager.getDeviceService();
-			this.blackListIdentityService = serviceManager.getBlackListService();
+			this.blockedContactsService = serviceManager.getBlockedContactsService();
 			this.groupInviteService = serviceManager.getGroupInviteService();
 			this.groupCallManager = serviceManager.getGroupCallManager();
 		} catch (ThreemaException e) {
@@ -333,7 +333,7 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 			return;
 		}
 
-		if (this.deviceService == null || this.blackListIdentityService == null) {
+		if (this.deviceService == null || this.blockedContactsService == null) {
 			finish();
 			return;
 		}
@@ -1167,7 +1167,7 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 			items.add(new SelectorDialogItem(String.format(getString(R.string.chat_with), shortName), R.drawable.ic_chat_bubble));
 			optionsMap.add(SELECTOR_OPTION_CHAT);
 
-			if (ContactUtil.canReceiveVoipMessages(contactModel, blackListIdentityService)
+			if (ContactUtil.canReceiveVoipMessages(contactModel, blockedContactsService)
 				&& ConfigUtils.isCallsEnabled()
 			) {
 				items.add(new SelectorDialogItem(String.format(getString(R.string.call_with), shortName), R.drawable.ic_phone_locked_outline));

+ 31 - 3
app/src/main/java/ch/threema/app/activities/HomeActivity.java

@@ -55,6 +55,8 @@ import android.widget.Toast;
 import androidx.activity.result.ActivityResultLauncher;
 import androidx.activity.result.contract.ActivityResultContracts;
 import androidx.annotation.AnyThread;
+import androidx.annotation.ColorInt;
+import androidx.annotation.IdRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
@@ -588,7 +590,10 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
 						BottomNavigationView bottomNavigationView = findViewById(R.id.bottom_navigation);
 						if (bottomNavigationView != null) {
 							BadgeDrawable badgeDrawable = bottomNavigationView.getOrCreateBadge(R.id.contacts);
-							badgeDrawable.setBackgroundColor(ConfigUtils.getAccentColor(HomeActivity.this));
+                            // the contacts tab item badge uses custom colors (normally badges are red)
+                            badgeDrawable.setBackgroundColor(
+                                getContactsTabBadgeColor(bottomNavigationView.getSelectedItemId())
+                            );
 							if (badgeDrawable.getVerticalOffset() == 0) {
 								badgeDrawable.setVerticalOffset(getResources().getDimensionPixelSize(R.dimen.bottom_nav_badge_offset_vertical));
 							}
@@ -1275,6 +1280,12 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
 		this.bottomNavigationView.setOnNavigationItemSelectedListener(item -> {
 			showMainContent();
 
+            // the contacts tab item badge uses custom colors (normally badges are red)
+            final @Nullable BadgeDrawable badgeDrawableContacts = bottomNavigationView.getBadge(R.id.contacts);
+            if (badgeDrawableContacts != null) {
+                badgeDrawableContacts.setBackgroundColor(getContactsTabBadgeColor(item.getItemId()));
+            }
+
 			Fragment currentFragment = getSupportFragmentManager().findFragmentByTag(currentFragmentTag);
 			if (currentFragment != null) {
 				if (item.getItemId() == R.id.contacts) {
@@ -1462,7 +1473,7 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
 		// Inflate the menu; this adds items to the action bar if it is present.
 		getMenuInflater().inflate(R.menu.activity_home, menu);
 
-		ConfigUtils.addIconsToOverflowMenu(this, menu);
+		ConfigUtils.addIconsToOverflowMenu(menu);
 
 		return true;
 	}
@@ -1567,7 +1578,7 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
 					privateChatToggleMenuItem.setIcon(R.drawable.ic_outline_visibility_off);
 					privateChatToggleMenuItem.setTitle(R.string.title_hide_private_chats);
 				}
-				ConfigUtils.tintMenuItem(this, privateChatToggleMenuItem, R.attr.colorOnSurface);
+				ConfigUtils.tintMenuIcon(this, privateChatToggleMenuItem, R.attr.colorOnSurface);
 			}
 
 			Boolean addDisabled;
@@ -2015,6 +2026,23 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
 		}
 	}
 
+    /**
+     * @return The correct color to ensure contrast. If the navigation bar item is <strong>not</strong> selected, we use the {@code onSurfaceVariant}
+     * because the material library paints icons in {@code onSurfaceVariant}. So we match. If the item is selected, we have to use
+     * {@code onPrimaryContainer} since we customized our navigation bar active indicator to have the color of {@code primaryContainer}.
+     *
+     * @see "@style/Threema.BottomNavigationView"
+     */
+    @ColorInt
+    private int getContactsTabBadgeColor(@IdRes int currentlySelectedMenuItemId) {
+        return ConfigUtils.getColorFromAttribute(
+            HomeActivity.this,
+            currentlySelectedMenuItemId == R.id.contacts
+                ? R.attr.colorOnPrimaryContainer
+                : R.attr.colorOnSurfaceVariant
+        );
+    }
+
 	@Override
 	protected void onSaveInstanceState(@NonNull Bundle outState) {
 		try {

+ 241 - 258
app/src/main/java/ch/threema/app/activities/IdentityListActivity.java

@@ -26,7 +26,6 @@ import android.os.Parcelable;
 import android.text.InputType;
 import android.view.Menu;
 import android.view.MenuItem;
-import android.view.View;
 import android.view.ViewGroup;
 
 import androidx.annotation.NonNull;
@@ -40,16 +39,16 @@ import androidx.recyclerview.widget.RecyclerView;
 
 import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
 
-import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.adapters.IdentityListAdapter;
 import ch.threema.app.dialogs.TextEntryDialog;
 import ch.threema.app.exceptions.FileSystemNotPresentException;
-import ch.threema.app.listeners.ContactListener;
 import ch.threema.app.managers.ListenerManager;
 import ch.threema.app.services.ContactService;
 import ch.threema.app.services.IdListService;
@@ -57,261 +56,245 @@ import ch.threema.app.ui.EmptyRecyclerView;
 import ch.threema.app.ui.EmptyView;
 import ch.threema.domain.protocol.csp.ProtocolDefines;
 import ch.threema.localcrypto.MasterKeyLockedException;
-import ch.threema.storage.models.ContactModel;
 
 abstract public class IdentityListActivity extends ThreemaToolbarActivity implements TextEntryDialog.TextEntryDialogClickListener {
-	private static final String BUNDLE_RECYCLER_LAYOUT = "recycler";
-	private static final String BUNDLE_SELECTED_ITEM = "item";
-
-	private IdentityListAdapter adapter;
-	private ActionMode actionMode = null;
-	private Bundle savedInstanceState;
-	private String[] blackListedIdentities;
-	private EmptyRecyclerView recyclerView;
-	private ContactService contactService;
-
-	abstract protected IdListService getIdentityListService();
-
-	@Override
-	public void onCreate(Bundle savedInstanceState) {
-		super.onCreate(savedInstanceState);
-
-		try {
-			contactService = ThreemaApplication.getServiceManager().getContactService();
-		} catch (MasterKeyLockedException | FileSystemNotPresentException e) {
-			finish();
-		}
-
-		this.savedInstanceState = savedInstanceState;
-
-		ActionBar actionBar = getSupportActionBar();
-		actionBar.setDisplayHomeAsUpEnabled(true);
-		actionBar.setTitle(this.getTitleText());
-
-		recyclerView = this.findViewById(R.id.recycler);
-		recyclerView.setHasFixedSize(true);
-		recyclerView.setLayoutManager(new LinearLayoutManager(this));
-		recyclerView.setItemAnimator(new DefaultItemAnimator());
-		recyclerView.addItemDecoration(new DividerItemDecoration(this, LinearLayoutManager.VERTICAL));
-
-		ExtendedFloatingActionButton floatingActionButton = findViewById(R.id.floating);
-		floatingActionButton.show();
-		floatingActionButton.setOnClickListener(new View.OnClickListener() {
-			@Override
-			public void onClick(View v) {
-				startExclude();
-			}
-		});
-
-		// add text view if contact list is empty
-		EmptyView emptyView = new EmptyView(this);
-		emptyView.setup(this.getBlankListText());
-		((ViewGroup) recyclerView.getParent()).addView(emptyView);
-		recyclerView.setEmptyView(emptyView);
-		recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
-			@Override
-			public void onScrolled(RecyclerView recyclerView, int dx, int dy){
-				super.onScrolled(recyclerView, dx, dy);
-
-				if (dy >0) {
-					// Scroll Down
-					if (floatingActionButton.isShown()) {
-						floatingActionButton.shrink();
-					}
-				}
-				else if (dy <0) {
-					// Scroll Up
-					if (floatingActionButton.isShown()) {
-						floatingActionButton.extend();
-					}
-				}
-			}
-		});
-
-		this.updateListAdapter();
-
-	}
-
-	@Override
-	public int getLayoutResource() {
-		return R.layout.activity_recycler_toolbar;
-	}
-
-	protected abstract String getBlankListText();
-	protected abstract String getTitleText();
-
-	private void updateListAdapter() {
-		if(this.getIdentityListService() == null) {
-			//do nothing;
-			return;
-		}
-		this.blackListedIdentities = this.getIdentityListService().getAll();
-		List<IdentityListAdapter.Entity> blackList = new ArrayList<>();
-
-		for (String s: this.blackListedIdentities) {
-			blackList.add(new IdentityListAdapter.Entity(s));
-		}
-
-		if (adapter == null) {
-			adapter = new IdentityListAdapter(this);
-			adapter.setOnItemClickListener(new IdentityListAdapter.OnItemClickListener() {
-				@Override
-				public void onItemClick(IdentityListAdapter.Entity entity) {
-					if (entity.equals(adapter.getSelected())) {
-						if (actionMode != null) {
-							actionMode.finish();
-						}
-					} else {
-						adapter.setSelected(entity);
-						if (actionMode == null) {
-							actionMode = startSupportActionMode(new IdentityListAction());
-						}
-					}
-				}
-			});
-			recyclerView.setAdapter(adapter);
-		}
-		adapter.setData(blackList);
-
-		// restore after rotate
-		if(savedInstanceState != null) {
-			Parcelable savedRecyclerLayoutState = savedInstanceState.getParcelable(BUNDLE_RECYCLER_LAYOUT);
-			recyclerView.getLayoutManager().onRestoreInstanceState(savedRecyclerLayoutState);
-
-			String selectedIdentity = savedInstanceState.getString(BUNDLE_SELECTED_ITEM);
-			if (selectedIdentity != null) {
-				Iterator<IdentityListAdapter.Entity> iterator = blackList.iterator();
-				while (iterator.hasNext()) {
-					IdentityListAdapter.Entity entity = iterator.next();
-					if (selectedIdentity.equals(entity.getText())) {
-						adapter.setSelected(entity);
-						this.actionMode = startSupportActionMode(new IdentityListAction());
-						break;
-					}
-					iterator.remove();
-				}
-			}
-			savedInstanceState = null;
-		}
-	}
-
-	@Override
-	public boolean onOptionsItemSelected(MenuItem item) {
-		switch (item.getItemId()) {
-			case android.R.id.home:
-				finish();
-				break;
-		}
-		return true;
-	}
-
-	private void startExclude() {
-		if (actionMode != null) {
-			actionMode.finish();
-		}
-
-		DialogFragment dialogFragment = TextEntryDialog.newInstance(
-				R.string.title_enter_id,
-				R.string.enter_id_hint,
-				R.string.ok,
-				R.string.cancel,
-				"",
-				InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS,
-				TextEntryDialog.INPUT_FILTER_TYPE_IDENTITY);
-		dialogFragment.show(getSupportFragmentManager(), "excludeDialog");
-	}
-
-	private void excludeIdentity(String identity) {
-		if(this.getIdentityListService() == null) {
-			return;
-		}
-
-		//add identity to list!
-		this.getIdentityListService()
-				.add(identity);
-
-		fireOnModifiedContact(identity);
-
-		this.updateListAdapter();
-	}
-
-	private void removeIdentity(String identity) {
-		if(this.getIdentityListService() == null) {
-			return;
-		}
-
-		//remove identity from list!
-		this.getIdentityListService().remove(identity);
-
-		fireOnModifiedContact(identity);
-
-		this.updateListAdapter();
-	}
-
-	@Override
-	public void onSaveInstanceState(Bundle outState) {
-		super.onSaveInstanceState(outState);
-		if (recyclerView != null && adapter != null) {
-			outState.putParcelable(BUNDLE_RECYCLER_LAYOUT, recyclerView.getLayoutManager().onSaveInstanceState());
-			if (adapter.getSelected() != null) {
-				outState.putString(BUNDLE_SELECTED_ITEM, adapter.getSelected().getText());
-			}
-		}
-	}
-
-	public class IdentityListAction implements ActionMode.Callback {
-
-		public IdentityListAction() {
-		}
-
-		@Override
-		public boolean onCreateActionMode(ActionMode mode, Menu menu) {
-			mode.getMenuInflater().inflate(R.menu.action_identity_list, menu);
-			return true;
-		}
-
-
-		@Override
-		public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
-			return false;
-		}
-
-		@Override
-		public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
-			if (item.getItemId() == R.id.menu_identity_remove) {
-				IdentityListAdapter.Entity selectedEntity = adapter.getSelected();
-				if (selectedEntity != null) {
-					removeIdentity(selectedEntity.getText());
-				}
-				mode.finish();
-				return true;
-			}
-			return false;
-		}
-
-		@Override
-		public void onDestroyActionMode(ActionMode mode) {
-			actionMode = null;
-
-			adapter.clearSelection();
-		}
-	}
-
-	private void fireOnModifiedContact(final String identity) {
-		if (contactService != null) {
-			ListenerManager.contactListeners.handle(listener -> listener.onModified(identity));
-		}
-	}
-
-	public void onYes(@NonNull String tag, final @NonNull String text) {
-		if (text.length() == ProtocolDefines.IDENTITY_LEN) {
-			excludeIdentity(text);
-		}
-	}
-
-	@Override
-	public void onNo(String tag) {}
-
-	@Override
-	public void onNeutral(String tag) {}
+    private static final String BUNDLE_RECYCLER_LAYOUT = "recycler";
+    private static final String BUNDLE_SELECTED_ITEM = "item";
+
+    private IdentityListAdapter adapter;
+    private ActionMode actionMode = null;
+    private Bundle savedInstanceState;
+    private EmptyRecyclerView recyclerView;
+    private ContactService contactService;
+
+    abstract protected IdListService getIdentityListService();
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        try {
+            contactService = ThreemaApplication.requireServiceManager().getContactService();
+        } catch (MasterKeyLockedException | FileSystemNotPresentException e) {
+            finish();
+        }
+
+        this.savedInstanceState = savedInstanceState;
+
+        ActionBar actionBar = getSupportActionBar();
+        actionBar.setDisplayHomeAsUpEnabled(true);
+        actionBar.setTitle(this.getTitleText());
+
+        recyclerView = this.findViewById(R.id.recycler);
+        recyclerView.setHasFixedSize(true);
+        recyclerView.setLayoutManager(new LinearLayoutManager(this));
+        recyclerView.setItemAnimator(new DefaultItemAnimator());
+        recyclerView.addItemDecoration(new DividerItemDecoration(this, LinearLayoutManager.VERTICAL));
+
+        ExtendedFloatingActionButton floatingActionButton = findViewById(R.id.floating);
+        floatingActionButton.show();
+        floatingActionButton.setOnClickListener(v -> startExclude());
+
+        // add text view if contact list is empty
+        EmptyView emptyView = new EmptyView(this);
+        emptyView.setup(this.getBlankListText());
+        ((ViewGroup) recyclerView.getParent()).addView(emptyView);
+        recyclerView.setEmptyView(emptyView);
+        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+            @Override
+            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
+                super.onScrolled(recyclerView, dx, dy);
+
+                if (dy >0) {
+                    // Scroll Down
+                    if (floatingActionButton.isShown()) {
+                        floatingActionButton.shrink();
+                    }
+                }
+                else if (dy <0) {
+                    // Scroll Up
+                    if (floatingActionButton.isShown()) {
+                        floatingActionButton.extend();
+                    }
+                }
+            }
+        });
+
+        this.updateListAdapter();
+
+    }
+
+    @Override
+    public int getLayoutResource() {
+        return R.layout.activity_recycler_toolbar;
+    }
+
+    protected abstract String getBlankListText();
+    protected abstract String getTitleText();
+
+    private void updateListAdapter() {
+        if(this.getIdentityListService() == null) {
+            //do nothing;
+            return;
+        }
+
+        List<IdentityListAdapter.Entity> identityList = Arrays.stream(this.getIdentityListService().getAll())
+            .map(IdentityListAdapter.Entity::new)
+            .collect(Collectors.toList());
+
+        if (adapter == null) {
+            adapter = new IdentityListAdapter(this);
+            adapter.setOnItemClickListener(entity -> {
+                if (entity.equals(adapter.getSelected())) {
+                    if (actionMode != null) {
+                        actionMode.finish();
+                    }
+                } else {
+                    adapter.setSelected(entity);
+                    if (actionMode == null) {
+                        actionMode = startSupportActionMode(new IdentityListAction());
+                    }
+                }
+            });
+            recyclerView.setAdapter(adapter);
+        }
+        adapter.setData(identityList);
+
+        // restore after rotate
+        if(savedInstanceState != null) {
+            Parcelable savedRecyclerLayoutState = savedInstanceState.getParcelable(BUNDLE_RECYCLER_LAYOUT);
+            recyclerView.getLayoutManager().onRestoreInstanceState(savedRecyclerLayoutState);
+
+            String selectedIdentity = savedInstanceState.getString(BUNDLE_SELECTED_ITEM);
+            if (selectedIdentity != null) {
+                Iterator<IdentityListAdapter.Entity> iterator = identityList.iterator();
+                while (iterator.hasNext()) {
+                    IdentityListAdapter.Entity entity = iterator.next();
+                    if (selectedIdentity.equals(entity.getText())) {
+                        adapter.setSelected(entity);
+                        this.actionMode = startSupportActionMode(new IdentityListAction());
+                        break;
+                    }
+                    iterator.remove();
+                }
+            }
+            savedInstanceState = null;
+        }
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == android.R.id.home) {
+            finish();
+        }
+        return true;
+    }
+
+    private void startExclude() {
+        if (actionMode != null) {
+            actionMode.finish();
+        }
+
+        DialogFragment dialogFragment = TextEntryDialog.newInstance(
+            R.string.title_enter_id,
+            R.string.enter_id_hint,
+            R.string.ok,
+            R.string.cancel,
+            "",
+            InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS,
+            TextEntryDialog.INPUT_FILTER_TYPE_IDENTITY);
+        dialogFragment.show(getSupportFragmentManager(), "excludeDialog");
+    }
+
+    private void excludeIdentity(String identity) {
+        if(this.getIdentityListService() == null) {
+            return;
+        }
+
+        //add identity to list!
+        this.getIdentityListService()
+            .add(identity);
+
+        fireOnModifiedContact(identity);
+
+        this.updateListAdapter();
+    }
+
+    private void removeIdentity(String identity) {
+        if(this.getIdentityListService() == null) {
+            return;
+        }
+
+        //remove identity from list!
+        this.getIdentityListService().remove(identity);
+
+        fireOnModifiedContact(identity);
+
+        this.updateListAdapter();
+    }
+
+    @Override
+    public void onSaveInstanceState(@NonNull Bundle outState) {
+        super.onSaveInstanceState(outState);
+        if (recyclerView != null && adapter != null) {
+            outState.putParcelable(BUNDLE_RECYCLER_LAYOUT, recyclerView.getLayoutManager().onSaveInstanceState());
+            if (adapter.getSelected() != null) {
+                outState.putString(BUNDLE_SELECTED_ITEM, adapter.getSelected().getText());
+            }
+        }
+    }
+
+    public class IdentityListAction implements ActionMode.Callback {
+
+        public IdentityListAction() {
+        }
+
+        @Override
+        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+            mode.getMenuInflater().inflate(R.menu.action_identity_list, menu);
+            return true;
+        }
+
+
+        @Override
+        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+            return false;
+        }
+
+        @Override
+        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+            if (item.getItemId() == R.id.menu_identity_remove) {
+                IdentityListAdapter.Entity selectedEntity = adapter.getSelected();
+                if (selectedEntity != null) {
+                    removeIdentity(selectedEntity.getText());
+                }
+                mode.finish();
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void onDestroyActionMode(ActionMode mode) {
+            actionMode = null;
+
+            adapter.clearSelection();
+        }
+    }
+
+    private void fireOnModifiedContact(final String identity) {
+        if (contactService != null) {
+            ListenerManager.contactListeners.handle(listener -> listener.onModified(identity));
+        }
+    }
+
+    @Override
+    public void onYes(@NonNull String tag, final @NonNull String text) {
+        if (text.length() == ProtocolDefines.IDENTITY_LEN) {
+            excludeIdentity(text);
+        }
+    }
+
+    @Override
+    public void onNo(String tag) {}
 }

+ 9 - 9
app/src/main/java/ch/threema/app/activities/ImagePaintActivity.java

@@ -941,26 +941,26 @@ public class ImagePaintActivity extends ThreemaToolbarActivity implements Generi
 				break;
 		}
 
-		ConfigUtils.tintMenuItem(this, drawParentItem, R.attr.colorOnSurface);
-		ConfigUtils.tintMenuItem(this, paintItem, R.attr.colorOnSurface);
-		ConfigUtils.tintMenuItem(this, pencilItem, R.attr.colorOnSurface);
-		ConfigUtils.tintMenuItem(this, highlighterItem, R.attr.colorOnSurface);
+		ConfigUtils.tintMenuIcon(this, drawParentItem, R.attr.colorOnSurface);
+		ConfigUtils.tintMenuIcon(this, paintItem, R.attr.colorOnSurface);
+		ConfigUtils.tintMenuIcon(this, pencilItem, R.attr.colorOnSurface);
+		ConfigUtils.tintMenuIcon(this, highlighterItem, R.attr.colorOnSurface);
 
 		if (motionView.getSelectedEntity() == null) {
 			// no selected entities => draw mode or neutral mode
 			if (paintView.getActive()) {
 				switch (this.strokeMode) {
 					case STROKE_MODE_PENCIL:
-						ConfigUtils.tintMenuItem(pencilItem, this.penColor);
+						ConfigUtils.tintMenuIcon(pencilItem, this.penColor);
 						break;
 					case STROKE_MODE_HIGHLIGHTER:
-						ConfigUtils.tintMenuItem(highlighterItem, this.penColor);
+						ConfigUtils.tintMenuIcon(highlighterItem, this.penColor);
 						break;
 					default:
-						ConfigUtils.tintMenuItem(paintItem, this.penColor);
+						ConfigUtils.tintMenuIcon(paintItem, this.penColor);
 						break;
 				}
-				ConfigUtils.tintMenuItem(drawParentItem, this.penColor);
+				ConfigUtils.tintMenuIcon(drawParentItem, this.penColor);
 			}
 		}
 		undoItem.setVisible(hasChanges());
@@ -1002,7 +1002,7 @@ public class ImagePaintActivity extends ThreemaToolbarActivity implements Generi
 			cropItem.setVisible(true);
 		}
 
-		ConfigUtils.addIconsToOverflowMenu(this, menu);
+		ConfigUtils.addIconsToOverflowMenu(menu);
 
 		return true;
 	}

+ 1 - 1
app/src/main/java/ch/threema/app/activities/MessageDetailsActivity.kt

@@ -242,7 +242,7 @@ class MessageDetailsActivity : ThreemaToolbarActivity(), DialogClickListener {
             false
         }
         toolbar.setTitle(getString(R.string.message_log_title))
-        ConfigUtils.addIconsToOverflowMenu(this, toolbar.getMenu())
+        ConfigUtils.addIconsToOverflowMenu(toolbar.getMenu())
     }
 
     private fun onToggleFormattingClicked(item: MenuItem) {

+ 13 - 8
app/src/main/java/ch/threema/app/activities/MessageDetailsViewModel.kt

@@ -166,7 +166,10 @@ fun AbstractMessageModel.toUiModel(myIdentity: String) = MessageUiModel(
     messageDetailsUiModel = this.toMessageDetailsUiModel()
 )
 
-private fun AbstractMessageModel.getAckUiModel(myIdentity: String) = let {
+/**
+ *  @return Only returns an instance of [AckUiModel] if there is at least one ack/dec for this [AbstractMessageModel].
+ */
+private fun AbstractMessageModel.getAckUiModel(myIdentity: String): AckUiModel? = let {
     if (this is GroupMessageModel) {
         val ackStates = groupMessageStates?.filter { groupMessageState -> groupMessageState.value == MessageState.USERACK.name }
         val decStates = groupMessageStates?.filter { groupMessageState -> groupMessageState.value == MessageState.USERDEC.name }
@@ -181,18 +184,20 @@ private fun AbstractMessageModel.getAckUiModel(myIdentity: String) = let {
         } else {
             UserReaction.NONE
         }
-
-        GroupAckUiModel(
-            ackState = GroupAckDecState(ackStates?.size ?: 0, ackUserReaction),
-            decState = GroupAckDecState(decStates?.size ?: 0, decUserReaction),
-        )
+        return@let if (ackUserReaction != UserReaction.NONE || decUserReaction != UserReaction.NONE) {
+            GroupAckUiModel(
+                ackState = GroupAckDecState(ackStates?.size ?: 0, ackUserReaction),
+                decState = GroupAckDecState(decStates?.size ?: 0, decUserReaction),
+            )
+        } else {
+            null
+        }
     } else {
         val contactAckDecState = when (this.state) {
             MessageState.USERACK -> ContactAckDecState.ACK
             MessageState.USERDEC -> ContactAckDecState.DEC
-            else -> ContactAckDecState.NONE
+            else -> return@let null
         }
-
         ContactAckUiModel(contactAckDecState)
     }
 }

+ 5 - 1
app/src/main/java/ch/threema/app/activities/StarredMessagesActivity.kt

@@ -189,7 +189,11 @@ class StarredMessagesActivity : ThreemaToolbarActivity(), SearchView.OnQueryText
         recyclerView.layoutManager = LinearLayoutManager(this)
         recyclerView.itemAnimator = DefaultItemAnimator()
         val emptyView = EmptyView(this, ConfigUtils.getActionBarSize(this))
-        emptyView.setup(R.string.no_starred_messages, R.drawable.ic_star_golden_24dp)
+        emptyView.setup(
+            R.string.no_starred_messages,
+            R.drawable.ic_star_golden_24dp,
+            null
+        )
         (recyclerView.parent.parent as ViewGroup).addView(emptyView)
         recyclerView.emptyView = emptyView
         emptyView.setLoading(true)

+ 4 - 3
app/src/main/java/ch/threema/app/adapters/ComposeMessageAdapter.java

@@ -24,6 +24,7 @@ package ch.threema.app.adapters;
 import static ch.threema.domain.protocol.csp.messages.file.FileData.RENDERING_DEFAULT;
 
 import android.content.Context;
+import android.content.res.ColorStateList;
 import android.text.TextUtils;
 import android.util.SparseIntArray;
 import android.view.LayoutInflater;
@@ -508,15 +509,15 @@ public class ComposeMessageAdapter extends ArrayAdapter<AbstractMessageModel> {
 					holder.secondaryTextView = itemView.findViewById(R.id.secondary_text_view);
 					holder.seekBar = itemView.findViewById(R.id.seek);
 					holder.tertiaryTextView = itemView.findViewById(R.id.tertiaryTextView);
-					holder.size = itemView.findViewById(R.id.document_size_view);
-					holder.controller = itemView.findViewById(R.id.controller);
+                    holder.size = itemView.findViewById(R.id.document_size_view);
+                    holder.controller = itemView.findViewById(R.id.controller);
 					holder.quoteBar = itemView.findViewById(R.id.quote_bar);
 					holder.quoteThumbnail = itemView.findViewById(R.id.quote_thumbnail);
 					holder.quoteTypeImage = itemView.findViewById(R.id.quote_type_image);
 					holder.transcoderView = itemView.findViewById(R.id.transcoder_view);
 					holder.readOnContainer = itemView.findViewById(R.id.read_on_container);
 					holder.readOnButton = itemView.findViewById(R.id.read_on_button);
-					holder.messageTypeButton = itemView.findViewById(R.id.message_type_button);
+					holder.audioMessageIcon = itemView.findViewById(R.id.audio_message_icon);
 					holder.groupAckContainer = itemView.findViewById(R.id.groupack_container);
 					holder.groupAckThumbsUpCount = itemView.findViewById(R.id.groupack_thumbsup_count);
 					holder.groupAckThumbsDownCount = itemView.findViewById(R.id.groupack_thumbsdown_count);

+ 3 - 4
app/src/main/java/ch/threema/app/adapters/ContactDetailAdapter.java

@@ -66,7 +66,6 @@ import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.ViewUtil;
 import ch.threema.base.utils.LoggingUtil;
 import ch.threema.data.models.ContactModelData;
-import ch.threema.domain.models.WorkVerificationLevel;
 import ch.threema.protobuf.csp.e2e.fs.Terminate;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.GroupModel;
@@ -93,7 +92,7 @@ public class ContactDetailAdapter extends RecyclerView.Adapter<RecyclerView.View
     private GroupService groupService;
     private PreferenceService preferenceService;
     private IdListService excludeFromSyncListService;
-    private IdListService blackListIdentityService;
+    private IdListService blockedContactsService;
     @Deprecated
     private final ContactModel contactModel;
     private final @NonNull ContactModelData contactModelData;
@@ -229,7 +228,7 @@ public class ContactDetailAdapter extends RecyclerView.Adapter<RecyclerView.View
             this.contactService = serviceManager.getContactService();
             this.groupService = serviceManager.getGroupService();
             this.excludeFromSyncListService = serviceManager.getExcludedSyncIdentitiesService();
-            this.blackListIdentityService = serviceManager.getBlackListService();
+            this.blockedContactsService = serviceManager.getBlockedContactsService();
             this.preferenceService = serviceManager.getPreferenceService();
         } catch (Exception e) {
             logger.error("Failed to set up services", e);
@@ -284,7 +283,7 @@ public class ContactDetailAdapter extends RecyclerView.Adapter<RecyclerView.View
             String identityAdditional = null;
             switch (this.contactModelData.activityState) {
                 case ACTIVE:
-                    if (blackListIdentityService.has(contactModelData.identity)) {
+                    if (blockedContactsService.has(contactModelData.identity)) {
                         identityAdditional = context.getString(R.string.blocked);
                     }
                     break;

+ 4 - 4
app/src/main/java/ch/threema/app/adapters/ContactListAdapter.java

@@ -80,7 +80,7 @@ public class ContactListAdapter extends FilterableListAdapter implements Section
 
     private final ContactService contactService;
     private final PreferenceService preferenceService;
-    private final IdListService blackListIdentityService;
+    private final IdListService blockedContactsService;
 
     public static final int VIEW_TYPE_NORMAL = 0;
     public static final int VIEW_TYPE_RECENTLY_ADDED = 1;
@@ -116,7 +116,7 @@ public class ContactListAdapter extends FilterableListAdapter implements Section
         @NonNull List<ContactModel> values,
         ContactService contactService,
         PreferenceService preferenceService,
-        IdListService blackListIdentityService,
+        IdListService blockedContactsService,
         AvatarListener avatarListener,
         @NonNull RequestManager requestManager
     ) {
@@ -126,7 +126,7 @@ public class ContactListAdapter extends FilterableListAdapter implements Section
         this.ovalues = this.values;
         this.contactService = contactService;
         this.preferenceService = preferenceService;
-        this.blackListIdentityService = blackListIdentityService;
+        this.blockedContactsService = blockedContactsService;
         this.avatarListener = avatarListener;
         this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
 
@@ -404,7 +404,7 @@ public class ContactListAdapter extends FilterableListAdapter implements Section
 
         ViewUtil.show(
             holder.blockedContactView,
-            blackListIdentityService != null && blackListIdentityService.has(contactModel.getIdentity())
+            blockedContactsService != null && blockedContactsService.has(contactModel.getIdentity())
         );
 
         if (viewType == VIEW_TYPE_RECENTLY_ADDED) {

+ 10 - 4
app/src/main/java/ch/threema/app/adapters/MessageListViewHolder.kt

@@ -370,9 +370,10 @@ class MessageListViewHolder(
         dateView.contentDescription = null
     }
 
-    private fun initializeDeliveryView(messageListAdapterItem: MessageListAdapterItem,
-                                       isHiddenChat: Boolean,
-                                       hasDraft: Boolean
+    private fun initializeDeliveryView(
+        messageListAdapterItem: MessageListAdapterItem,
+        isHiddenChat: Boolean,
+        hasDraft: Boolean
     ) {
         if (isHiddenChat || hasDraft) {
             deliveryView.visibility = GONE
@@ -399,7 +400,12 @@ class MessageListViewHolder(
                 if (messageListAdapterItem.latestMessage != null) {
                     // In case there is a latest message but no icon is set, we need to get the
                     // icon for the current message state
-                    params.stateBitmapUtil?.setStateDrawable(context, messageListAdapterItem.latestMessage, deliveryView, false)
+                    params.stateBitmapUtil?.setStateDrawable(
+                        context,
+                        messageListAdapterItem.latestMessage,
+                        deliveryView,
+                        ConfigUtils.getColorFromAttribute(context, R.attr.colorOnSurface)
+                    )
                 } else {
                     deliveryView.visibility = GONE
                 }

+ 4 - 4
app/src/main/java/ch/threema/app/adapters/UserListAdapter.java

@@ -70,7 +70,7 @@ public class UserListAdapter extends FilterableListAdapter {
 	private final List<ContactModel> originalValues;
 	private UserListFilter userListFilter;
 	private final ContactService contactService;
-	private final IdListService blacklistService;
+	private final IdListService blockedContactsService;
 	private final DeadlineListService hiddenChatsListService;
 	private final FilterResultsListener filterResultsListener;
 	private final @NonNull RequestManager requestManager;
@@ -81,7 +81,7 @@ public class UserListAdapter extends FilterableListAdapter {
 		@Nullable final List<String> preselectedIdentities,
 		final List<Integer> checkedItems,
 		ContactService contactService,
-		IdListService blacklistService,
+		IdListService blockedContactsService,
 		DeadlineListService hiddenChatsListService,
 		PreferenceService preferenceService,
 		FilterResultsListener filterResultsListener,
@@ -92,7 +92,7 @@ public class UserListAdapter extends FilterableListAdapter {
 		this.context = context;
 		this.contactService = contactService;
 		this.hiddenChatsListService = hiddenChatsListService;
-		this.blacklistService = blacklistService;
+		this.blockedContactsService = blockedContactsService;
 		this.filterResultsListener = filterResultsListener;
 		this.requestManager = requestManager;
 
@@ -168,7 +168,7 @@ public class UserListAdapter extends FilterableListAdapter {
 
 		ViewUtil.show(
 				holder.blockedView,
-				blacklistService != null && blacklistService.has(contactModel.getIdentity())
+				blockedContactsService != null && blockedContactsService.has(contactModel.getIdentity())
 		);
 
 		holder.verificationLevelView.setContactModel(contactModel);

+ 13 - 2
app/src/main/java/ch/threema/app/adapters/decorators/AudioChatAdapterDecorator.java

@@ -23,10 +23,12 @@ package ch.threema.app.adapters.decorators;
 
 import android.annotation.SuppressLint;
 import android.content.Context;
+import android.content.res.ColorStateList;
 import android.view.View;
 import android.widget.SeekBar;
 import android.widget.Toast;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.UiThread;
 
 import org.slf4j.Logger;
@@ -68,6 +70,15 @@ public class AudioChatAdapterDecorator extends ChatAdapterDecorator {
         logger.info("New AudioChatAdapterDecorator instance for {}", messageModel.getId());
     }
 
+    @Override
+    protected void applyContentColor(
+        final @NonNull ComposeMessageHolder viewHolder,
+        final @NonNull ColorStateList contentColor
+    ) {
+        super.applyContentColor(viewHolder, contentColor);
+        viewHolder.audioMessageIcon.setImageTintList(getMessageModel().getUiContentColor(getContext()));
+    }
+
     @Override
     protected void configureChatMessage(final ComposeMessageHolder holder, final int position) {
 
@@ -108,7 +119,7 @@ public class AudioChatAdapterDecorator extends ChatAdapterDecorator {
         holder.seekBar.setMessageModel(getMessageModel(), helper.getThumbnailCache());
         holder.seekBar.setEnabled(false);
         holder.readOnButton.setVisibility(View.GONE);
-        holder.messageTypeButton.setVisibility(View.VISIBLE);
+        holder.audioMessageIcon.setVisibility(View.VISIBLE);
         holder.controller.setOnClickListener(v -> {
             int status = holder.controller.getStatus();
 
@@ -379,7 +390,7 @@ public class AudioChatAdapterDecorator extends ChatAdapterDecorator {
     private synchronized void changePlayingState(final ComposeMessageHolder holder, boolean isPlaying) {
         logger.debug("changePlayingState for {} to {}", getMessageModel().getId(), isPlaying);
         holder.readOnButton.setVisibility(isPlaying ? View.VISIBLE : View.GONE);
-        holder.messageTypeButton.setVisibility(isPlaying ? View.GONE : View.VISIBLE);
+        holder.audioMessageIcon.setVisibility(isPlaying ? View.GONE : View.VISIBLE);
     }
 
     @SuppressLint("DefaultLocale")

+ 1 - 1
app/src/main/java/ch/threema/app/adapters/decorators/BallotChatAdapterDecorator.java

@@ -100,7 +100,7 @@ public class BallotChatAdapterDecorator extends ChatAdapterDecorator {
 			}, holder.messageBlockView);
 
 			if (holder.controller != null) {
-				holder.controller.setImageResource(R.drawable.ic_outline_rule);
+				holder.controller.setIconResource(R.drawable.ic_outline_rule);
 			}
 
 			RuntimeUtil.runOnUiThread(() -> setupResendStatus(holder));

+ 563 - 520
app/src/main/java/ch/threema/app/adapters/decorators/ChatAdapterDecorator.java

@@ -22,6 +22,7 @@
 package ch.threema.app.adapters.decorators;
 
 import android.content.Context;
+import android.content.res.ColorStateList;
 import android.graphics.Bitmap;
 import android.graphics.Color;
 import android.text.Spannable;
@@ -39,6 +40,7 @@ import com.google.android.material.imageview.ShapeableImageView;
 import com.google.android.material.shape.ShapeAppearanceModel;
 import com.google.common.util.concurrent.ListenableFuture;
 
+import org.jetbrains.annotations.MustBeInvokedByOverriders;
 import org.slf4j.Logger;
 
 import java.util.HashMap;
@@ -75,528 +77,569 @@ import ch.threema.storage.models.MessageType;
 import ch.threema.storage.models.data.DisplayTag;
 
 abstract public class ChatAdapterDecorator extends AdapterDecorator {
-	private static final Logger logger = LoggingUtil.getThreemaLogger("ChatAdapterDecorator");
-
-	public interface OnClickElement {
-		void onClick(AbstractMessageModel messageModel);
-	}
-
-	public interface OnLongClickElement {
-		void onLongClick(AbstractMessageModel messageModel);
-	}
-
-	public interface OnTouchElement {
-		boolean onTouch(MotionEvent motionEvent, AbstractMessageModel messageModel);
-	}
-
-	public interface ActionModeStatus {
-		boolean getActionModeEnabled();
-	}
-
-	private final AbstractMessageModel messageModel;
-	protected final Helper helper;
-	private final StateBitmapUtil stateBitmapUtil;
-
-	protected OnClickElement onClickElement = null;
-	private OnLongClickElement onLongClickElement = null;
-	private OnTouchElement onTouchElement = null;
-	protected ActionModeStatus actionModeStatus = null;
-	private long durationS = 0;
-	private CharSequence datePrefix = "";
-	protected String dateContentDescriptionPrefix = "";
-
-	private int groupId = 0;
-	protected Map<String, Integer> identityColors = null;
-	protected String filterString;
-
-	// whether this message should be displayed as a continuation of a previous message by the same sender
-	private boolean isGroupedMessage = true;
-
-	public static class ContactCache {
-		public String identity;
-		public String displayName;
-		public Bitmap avatar;
-		public ContactModel contactModel;
-	}
-
-	public static class Helper {
-		private final String myIdentity;
-		private final MessageService messageService;
-		private final UserService userService;
-		private final ContactService contactService;
-		private final FileService fileService;
-		private final MessagePlayerService messagePlayerService;
-		private final BallotService ballotService;
-		private final ThumbnailCache thumbnailCache;
-		private final PreferenceService preferenceService;
-		private final DownloadService downloadService;
-		private final LicenseService licenseService;
-		private MessageReceiver messageReceiver;
-		private int thumbnailWidth;
-		private final Fragment fragment;
-		protected int regularColor;
-		private final Map<String, ContactCache> contacts = new HashMap<>();
-		private final int maxBubbleTextLength;
-		private final int maxQuoteTextLength;
-		private final ListenableFuture<MediaController> mediaControllerFuture;
-
-		public Helper(
-			String myIdentity,
-			MessageService messageService,
-			UserService userService,
-			ContactService contactService,
-			FileService fileService,
-			MessagePlayerService messagePlayerService,
-			BallotService ballotService,
-			ThumbnailCache thumbnailCache,
-			PreferenceService preferenceService,
-			DownloadService downloadService,
-			LicenseService licenseService,
-			MessageReceiver messageReceiver,
-			int thumbnailWidth,
-			Fragment fragment,
-			int regularColor,
-			int maxBubbleTextLength,
-			int maxQuoteTextLength,
-			ListenableFuture<MediaController> mediaControllerFuture) {
-			this.myIdentity = myIdentity;
-			this.messageService = messageService;
-			this.userService = userService;
-			this.contactService = contactService;
-			this.fileService = fileService;
-			this.messagePlayerService = messagePlayerService;
-			this.ballotService = ballotService;
-			this.thumbnailCache = thumbnailCache;
-			this.preferenceService = preferenceService;
-			this.downloadService = downloadService;
-			this.licenseService = licenseService;
-			this.messageReceiver = messageReceiver;
-			this.thumbnailWidth = thumbnailWidth;
-			this.fragment = fragment;
-			this.regularColor = regularColor;
-			this.maxBubbleTextLength = maxBubbleTextLength;
-			this.maxQuoteTextLength = maxQuoteTextLength;
-			this.mediaControllerFuture = mediaControllerFuture;
-		}
-
-		public Fragment getFragment() {
-			return fragment;
-		}
-
-		public int getThumbnailWidth() {
-			return thumbnailWidth;
-		}
-
-		public ThumbnailCache getThumbnailCache() {
-			return thumbnailCache;
-		}
-
-		public MessagePlayerService getMessagePlayerService() {
-			return messagePlayerService;
-		}
-
-		public FileService getFileService() {
-			return fileService;
-		}
-
-		public UserService getUserService() {
-			return userService;
-		}
-
-		public ContactService getContactService() {
-			return contactService;
-		}
-
-		public MessageService getMessageService() {
-			return messageService;
-		}
-
-		public PreferenceService getPreferenceService() {
-			return preferenceService;
-		}
-
-		public DownloadService getDownloadService() {
-			return downloadService;
-		}
-
-		public LicenseService getLicenseService() {
-			return licenseService;
-		}
-
-		public String getMyIdentity() {
-			return myIdentity;
-		}
-
-		public BallotService getBallotService() {
-			return ballotService;
-		}
-
-		public Map<String, ContactCache> getContactCache() {
-			return contacts;
-		}
-
-		public MessageReceiver getMessageReceiver() {
-			return messageReceiver;
-		}
-
-		public void setThumbnailWidth(int preferredThumbnailWidth) {
-			thumbnailWidth = preferredThumbnailWidth;
-		}
-
-		public int getMaxBubbleTextLength() {
-			return maxBubbleTextLength;
-		}
-
-		public int getMaxQuoteTextLength() {
-			return maxQuoteTextLength;
-		}
-
-		public void setMessageReceiver(MessageReceiver messageReceiver) {
-			this.messageReceiver = messageReceiver;
-		}
-
-		public ListenableFuture<MediaController> getMediaControllerFuture() {
-			return this.mediaControllerFuture;
-		}
-	}
-
-	public ChatAdapterDecorator(
-		@NonNull Context context,
-		AbstractMessageModel messageModel,
-		Helper helper
-	) {
-		super(context);
-		this.messageModel = messageModel;
-		this.helper = helper;
-		stateBitmapUtil = StateBitmapUtil.getInstance();
-		try {
-			actionModeStatus = (ActionModeStatus) helper.getFragment();
-		} catch (ClassCastException e) {
-			throw new ClassCastException(context.toString()
-				+ " must implement ActionModeStatus");
-		}
-	}
-
-	public void setGroupMessage(int groupId, Map<String, Integer> identityColors) {
-		this.groupId = groupId;
-		this.identityColors = identityColors;
-	}
-
-	public void setOnClickElement(OnClickElement onClickElement) {
-		this.onClickElement = onClickElement;
-	}
-
-	public void setOnLongClickElement(OnLongClickElement onClickElement) {
-		onLongClickElement = onClickElement;
-	}
-
-	public void setOnTouchElement(OnTouchElement onTouchElement) {
-		this.onTouchElement = onTouchElement;
-	}
-
-	final public void setFilter(String filterString) {
-		this.filterString = filterString;
-	}
-
-	@Override
-	final protected void configure(final AbstractListItemHolder h, int position) {
-		if (!(h instanceof ComposeMessageHolder) || h.position != position) {
-			return;
-		}
-
-		boolean isUserMessage = !getMessageModel().isStatusMessage()
-			&& getMessageModel().getType() != MessageType.STATUS
-			&& getMessageModel().getType() != MessageType.GROUP_CALL_STATUS;
-
-		String identity = (
-			messageModel.isOutbox() ?
-				helper.getMyIdentity() :
-				messageModel.getIdentity());
-
-		final ComposeMessageHolder holder = (ComposeMessageHolder) h;
-
-		//configure the chat message
-		configureChatMessage(holder, position);
-
-		if (isUserMessage) {
-			if (!messageModel.isOutbox() && groupId > 0) {
-
-				ContactCache c = helper.getContactCache().get(identity);
-				if (c == null) {
-					ContactModel contactModel = helper.getContactService().getByIdentity(messageModel.getIdentity());
-					c = new ContactCache();
-					c.displayName = NameUtil.getDisplayNameOrNickname(contactModel, true);
-					c.avatar = helper.getContactService().getAvatar(contactModel, false);
-
-					c.contactModel = contactModel;
-					helper.getContactCache().put(identity, c);
-				}
-
-				if (holder.senderView != null) {
-					if (isGroupedMessage) {
-						holder.senderView.setVisibility(View.VISIBLE);
-						holder.senderName.setText(c.displayName);
-
-						if (identityColors != null && identityColors.containsKey(identity)) {
-							holder.senderName.setTextColor(identityColors.get(identity));
-						} else {
-							holder.senderName.setTextColor(helper.regularColor);
-						}
-					} else {
-						// hide sender name in grouped messages
-						holder.senderView.setVisibility(View.GONE);
-					}
-				}
-
-				if (holder.avatarView != null) {
-					if (isGroupedMessage) {
-						holder.avatarView.setImageBitmap(c.avatar);
-						holder.avatarView.setVisibility(View.VISIBLE);
-						if (c.contactModel != null) {
-							holder.avatarView.setBadgeVisible(helper.getContactService().showBadge(c.contactModel));
-						}
-					} else {
-						// hide avatar in grouped messages
-						holder.avatarView.setVisibility(View.INVISIBLE);
-					}
-				}
-			} else {
-				if (holder.avatarView != null) {
-					holder.avatarView.setVisibility(View.GONE);
-				}
-				if (holder.senderView != null) {
-					holder.senderView.setVisibility(View.GONE);
-				}
-			}
-
-			CharSequence s = MessageUtil.getDisplayDate(getContext(), messageModel, true);
-			if (s == null) {
-				s = "";
-			}
-
-			CharSequence contentDescription;
-
-			if (!TestUtil.isBlankOrNull(datePrefix)) {
-				contentDescription = getContext().getString(R.string.state_dialog_modified) + ": " + s;
-				if (messageModel.isOutbox()) {
-					s = TextUtils.concat(datePrefix, " | " + s);
-				} else {
-					s = TextUtils.concat(s + " | ", datePrefix);
-				}
-			} else {
-				contentDescription = s;
-			}
-			if (holder.dateView != null) {
-				holder.dateView.setText(s);
-				holder.dateView.setContentDescription(contentDescription);
-			}
-
-			if (holder.datePrefixIcon != null) {
-				holder.datePrefixIcon.setVisibility(durationS > 0L ? View.VISIBLE : View.GONE);
-			}
-
-			if (holder.starredIcon != null) {
-				holder.starredIcon.setVisibility((messageModel.getDisplayTags() & DisplayTag.DISPLAY_TAG_STARRED) == DisplayTag.DISPLAY_TAG_STARRED ? View.VISIBLE : View.GONE);
-			}
-
-			if (holder.deliveredIndicator != null) {
-				stateBitmapUtil.setStateDrawable(getContext(), messageModel, holder.deliveredIndicator, true);
-			}
-
-			if (holder.groupAckContainer != null) {
-				stateBitmapUtil.setGroupAckCount(messageModel, holder);
-			}
-
-			if (holder.editedText != null) {
-				holder.editedText.setVisibility(messageModel.getEditedAt() != null ? View.VISIBLE : View.GONE);
-			}
-		}
-	}
-
-	public Spannable highlightMatches(CharSequence fullText, String filterText) {
-		return TextUtil.highlightMatches(getContext(), fullText, filterText, true, false);
-	}
-
-	CharSequence formatTextString(@Nullable String string, String filterString) {
-		return formatTextString(string, filterString, -1);
-	}
-
-	CharSequence formatTextString(@Nullable String string, String filterString, int maxLength) {
-		if (TextUtils.isEmpty(string)) {
-			return "";
-		}
-
-		if (maxLength > 0 && string.length() > maxLength) {
-			return highlightMatches(string.substring(0, maxLength - 1), filterString);
-		}
-		return highlightMatches(string, filterString);
-	}
-
-	protected void configureChatMessage(final ComposeMessageHolder holder, final int position) {
-		if (holder.attachmentImage instanceof ShapeableImageView) {
-			ShapeAppearanceModel shapeAppearanceModel = new ShapeAppearanceModel.Builder()
-				.setAllCornerSizes(ImageViewUtil.getCornerRadius(getContext()))
-				.build();
-			((ShapeableImageView) holder.attachmentImage).setShapeAppearanceModel(shapeAppearanceModel);
-		}
-	}
-
-	protected void setDatePrefix(String prefix) {
-		datePrefix = prefix;
-	}
-
-	protected void setDuration(long durationS) {
-		this.durationS = durationS;
-	}
-
-	protected MessageService getMessageService() {
-		return helper.getMessageService();
-	}
-
-	protected MessagePlayerService getMessagePlayerService() {
-		return helper.getMessagePlayerService();
-	}
-
-	protected FileService getFileService() {
-		return helper.getFileService();
-	}
-
-	protected int getThumbnailWidth() {
-		return helper.getThumbnailWidth();
-	}
-
-	protected ThumbnailCache getThumbnailCache() {
-		return helper.getThumbnailCache();
-	}
-
-	protected AbstractMessageModel getMessageModel() {
-		return messageModel;
-	}
-
-	protected PreferenceService getPreferenceService() {
-		return helper.getPreferenceService();
-	}
-
-	protected UserService getUserService() {
-		return helper.getUserService();
-	}
-
-	protected ContactService getContactService() {
-		return helper.getContactService();
-	}
-
-	protected void setOnClickListener(final View.OnClickListener onViewClickListener, View view) {
-		if (view != null) {
-			view.setOnClickListener(v -> {
-				if (onViewClickListener != null && !actionModeStatus.getActionModeEnabled()) {
-					// do not propagate click if actionMode (selection mode) is enabled in parent
-					onViewClickListener.onClick(v);
-				}
-				if (onClickElement != null) {
-					//propagate event to parents
-					onClickElement.onClick(getMessageModel());
-				}
-			});
+    private static final Logger logger = LoggingUtil.getThreemaLogger("ChatAdapterDecorator");
+
+    public interface OnClickElement {
+        void onClick(AbstractMessageModel messageModel);
+    }
+
+    public interface OnLongClickElement {
+        void onLongClick(AbstractMessageModel messageModel);
+    }
+
+    public interface OnTouchElement {
+        boolean onTouch(MotionEvent motionEvent, AbstractMessageModel messageModel);
+    }
+
+    public interface ActionModeStatus {
+        boolean getActionModeEnabled();
+    }
+
+    private final AbstractMessageModel messageModel;
+    protected final Helper helper;
+    private final StateBitmapUtil stateBitmapUtil;
+
+    protected OnClickElement onClickElement = null;
+    private OnLongClickElement onLongClickElement = null;
+    private OnTouchElement onTouchElement = null;
+    protected ActionModeStatus actionModeStatus = null;
+    private long durationS = 0;
+    private CharSequence datePrefix = "";
+    protected String dateContentDescriptionPrefix = "";
+
+    private int groupId = 0;
+    protected Map<String, Integer> identityColors = null;
+    protected String filterString;
+
+    // whether this message should be displayed as a continuation of a previous message by the same sender
+    private boolean isGroupedMessage = true;
+
+    public static class ContactCache {
+        public String identity;
+        public String displayName;
+        public Bitmap avatar;
+        public ContactModel contactModel;
+    }
+
+    public static class Helper {
+        private final String myIdentity;
+        private final MessageService messageService;
+        private final UserService userService;
+        private final ContactService contactService;
+        private final FileService fileService;
+        private final MessagePlayerService messagePlayerService;
+        private final BallotService ballotService;
+        private final ThumbnailCache thumbnailCache;
+        private final PreferenceService preferenceService;
+        private final DownloadService downloadService;
+        private final LicenseService licenseService;
+        private MessageReceiver messageReceiver;
+        private int thumbnailWidth;
+        private final Fragment fragment;
+        protected int regularColor;
+        private final Map<String, ContactCache> contacts = new HashMap<>();
+        private final int maxBubbleTextLength;
+        private final int maxQuoteTextLength;
+        private final ListenableFuture<MediaController> mediaControllerFuture;
+
+        public Helper(
+            String myIdentity,
+            MessageService messageService,
+            UserService userService,
+            ContactService contactService,
+            FileService fileService,
+            MessagePlayerService messagePlayerService,
+            BallotService ballotService,
+            ThumbnailCache thumbnailCache,
+            PreferenceService preferenceService,
+            DownloadService downloadService,
+            LicenseService licenseService,
+            MessageReceiver messageReceiver,
+            int thumbnailWidth,
+            Fragment fragment,
+            int regularColor,
+            int maxBubbleTextLength,
+            int maxQuoteTextLength,
+            ListenableFuture<MediaController> mediaControllerFuture) {
+            this.myIdentity = myIdentity;
+            this.messageService = messageService;
+            this.userService = userService;
+            this.contactService = contactService;
+            this.fileService = fileService;
+            this.messagePlayerService = messagePlayerService;
+            this.ballotService = ballotService;
+            this.thumbnailCache = thumbnailCache;
+            this.preferenceService = preferenceService;
+            this.downloadService = downloadService;
+            this.licenseService = licenseService;
+            this.messageReceiver = messageReceiver;
+            this.thumbnailWidth = thumbnailWidth;
+            this.fragment = fragment;
+            this.regularColor = regularColor;
+            this.maxBubbleTextLength = maxBubbleTextLength;
+            this.maxQuoteTextLength = maxQuoteTextLength;
+            this.mediaControllerFuture = mediaControllerFuture;
+        }
+
+        public Fragment getFragment() {
+            return fragment;
+        }
+
+        public int getThumbnailWidth() {
+            return thumbnailWidth;
+        }
+
+        public ThumbnailCache getThumbnailCache() {
+            return thumbnailCache;
+        }
+
+        public MessagePlayerService getMessagePlayerService() {
+            return messagePlayerService;
+        }
+
+        public FileService getFileService() {
+            return fileService;
+        }
+
+        public UserService getUserService() {
+            return userService;
+        }
+
+        public ContactService getContactService() {
+            return contactService;
+        }
+
+        public MessageService getMessageService() {
+            return messageService;
+        }
+
+        public PreferenceService getPreferenceService() {
+            return preferenceService;
+        }
+
+        public DownloadService getDownloadService() {
+            return downloadService;
+        }
+
+        public LicenseService getLicenseService() {
+            return licenseService;
+        }
+
+        public String getMyIdentity() {
+            return myIdentity;
+        }
+
+        public BallotService getBallotService() {
+            return ballotService;
+        }
+
+        public Map<String, ContactCache> getContactCache() {
+            return contacts;
+        }
+
+        public MessageReceiver getMessageReceiver() {
+            return messageReceiver;
+        }
+
+        public void setThumbnailWidth(int preferredThumbnailWidth) {
+            thumbnailWidth = preferredThumbnailWidth;
+        }
+
+        public int getMaxBubbleTextLength() {
+            return maxBubbleTextLength;
+        }
+
+        public int getMaxQuoteTextLength() {
+            return maxQuoteTextLength;
+        }
+
+        public void setMessageReceiver(MessageReceiver messageReceiver) {
+            this.messageReceiver = messageReceiver;
+        }
+
+        public ListenableFuture<MediaController> getMediaControllerFuture() {
+            return this.mediaControllerFuture;
+        }
+    }
+
+    public ChatAdapterDecorator(
+        @NonNull Context context,
+        AbstractMessageModel messageModel,
+        Helper helper
+    ) {
+        super(context);
+        this.messageModel = messageModel;
+        this.helper = helper;
+        stateBitmapUtil = StateBitmapUtil.getInstance();
+        try {
+            actionModeStatus = (ActionModeStatus) helper.getFragment();
+        } catch (ClassCastException e) {
+            throw new ClassCastException(context.toString()
+                + " must implement ActionModeStatus");
+        }
+    }
+
+    public void setGroupMessage(int groupId, Map<String, Integer> identityColors) {
+        this.groupId = groupId;
+        this.identityColors = identityColors;
+    }
+
+    public void setOnClickElement(OnClickElement onClickElement) {
+        this.onClickElement = onClickElement;
+    }
+
+    public void setOnLongClickElement(OnLongClickElement onClickElement) {
+        onLongClickElement = onClickElement;
+    }
+
+    public void setOnTouchElement(OnTouchElement onTouchElement) {
+        this.onTouchElement = onTouchElement;
+    }
+
+    final public void setFilter(String filterString) {
+        this.filterString = filterString;
+    }
+
+    /**
+     *
+     * Is necessary because depending on the message model, we have to use a different color state list
+     *
+     * @param contentColor The color-state-list that will be applied to all {@code TextView} instances in {@code ComposeMessageHolder}
+     */
+    @MustBeInvokedByOverriders
+    protected void applyContentColor(
+        final @NonNull ComposeMessageHolder viewHolder,
+        final @NonNull ColorStateList contentColor
+    ) {
+        if (viewHolder.bodyTextView != null) {
+            viewHolder.bodyTextView.setTextColor(contentColor);
+        }
+        if (viewHolder.secondaryTextView != null) {
+            viewHolder.secondaryTextView.setTextColor(contentColor);
+        }
+        if (viewHolder.tertiaryTextView != null) {
+            viewHolder.tertiaryTextView.setTextColor(contentColor);
+        }
+        if (viewHolder.size != null) {
+            viewHolder.size.setTextColor(contentColor);
+        }
+        if (viewHolder.senderName != null) {
+            viewHolder.senderName.setTextColor(contentColor);
+        }
+        if (viewHolder.dateView != null) {
+            viewHolder.dateView.setTextColor(contentColor);
+        }
+        if (viewHolder.editedText != null) {
+            viewHolder.editedText.setTextColor(contentColor);
+        }
+    }
+
+    @Override
+    final protected void configure(final AbstractListItemHolder abstractViewHolder, int position) {
+        if (!(abstractViewHolder instanceof ComposeMessageHolder) || abstractViewHolder.position != position) {
+            return;
+        }
+
+        boolean isUserMessage = !getMessageModel().isStatusMessage()
+            && getMessageModel().getType() != MessageType.STATUS
+            && getMessageModel().getType() != MessageType.GROUP_CALL_STATUS;
+
+        String identity = messageModel.isOutbox()
+            ? helper.getMyIdentity()
+            : messageModel.getIdentity();
+
+        final @NonNull ComposeMessageHolder viewHolder = (ComposeMessageHolder) abstractViewHolder;
+
+        applyContentColor(viewHolder, getMessageModel().getUiContentColor(getContext()));
+
+        //configure the chat message
+        configureChatMessage(viewHolder, position);
+
+        if (isUserMessage) {
+            if (!messageModel.isOutbox() && groupId > 0) {
+
+                ContactCache c = helper.getContactCache().get(identity);
+                if (c == null) {
+                    ContactModel contactModel = helper.getContactService().getByIdentity(messageModel.getIdentity());
+                    c = new ContactCache();
+                    c.displayName = NameUtil.getDisplayNameOrNickname(contactModel, true);
+                    c.avatar = helper.getContactService().getAvatar(contactModel, false);
+
+                    c.contactModel = contactModel;
+                    helper.getContactCache().put(identity, c);
+                }
+
+                if (viewHolder.senderView != null) {
+                    if (isGroupedMessage) {
+                        viewHolder.senderView.setVisibility(View.VISIBLE);
+                        viewHolder.senderName.setText(c.displayName);
+
+                        if (identityColors != null && identityColors.containsKey(identity)) {
+                            viewHolder.senderName.setTextColor(identityColors.get(identity));
+                        } else {
+                            viewHolder.senderName.setTextColor(helper.regularColor);
+                        }
+                    } else {
+                        // hide sender name in grouped messages
+                        viewHolder.senderView.setVisibility(View.GONE);
+                    }
+                }
+
+                if (viewHolder.avatarView != null) {
+                    if (isGroupedMessage) {
+                        viewHolder.avatarView.setImageBitmap(c.avatar);
+                        viewHolder.avatarView.setVisibility(View.VISIBLE);
+                        if (c.contactModel != null) {
+                            viewHolder.avatarView.setBadgeVisible(helper.getContactService().showBadge(c.contactModel));
+                        }
+                    } else {
+                        // hide avatar in grouped messages
+                        viewHolder.avatarView.setVisibility(View.INVISIBLE);
+                    }
+                }
+            } else {
+                if (viewHolder.avatarView != null) {
+                    viewHolder.avatarView.setVisibility(View.GONE);
+                }
+                if (viewHolder.senderView != null) {
+                    viewHolder.senderView.setVisibility(View.GONE);
+                }
+            }
+
+            CharSequence s = MessageUtil.getDisplayDate(getContext(), messageModel, true);
+            if (s == null) {
+                s = "";
+            }
+
+            CharSequence contentDescription;
+
+            if (!TestUtil.isBlankOrNull(datePrefix)) {
+                contentDescription = getContext().getString(R.string.state_dialog_modified) + ": " + s;
+                if (messageModel.isOutbox()) {
+                    s = TextUtils.concat(datePrefix, " | " + s);
+                } else {
+                    s = TextUtils.concat(s + " | ", datePrefix);
+                }
+            } else {
+                contentDescription = s;
+            }
+            if (viewHolder.dateView != null) {
+                viewHolder.dateView.setText(s);
+                viewHolder.dateView.setContentDescription(contentDescription);
+            }
+
+            if (viewHolder.datePrefixIcon != null) {
+                viewHolder.datePrefixIcon.setVisibility(durationS > 0L ? View.VISIBLE : View.GONE);
+            }
+
+            if (viewHolder.starredIcon != null) {
+                viewHolder.starredIcon.setVisibility((messageModel.getDisplayTags() & DisplayTag.DISPLAY_TAG_STARRED) == DisplayTag.DISPLAY_TAG_STARRED ? View.VISIBLE : View.GONE);
+            }
+
+            if (viewHolder.deliveredIndicator != null) {
+                stateBitmapUtil.setStateDrawable(getContext(), messageModel, viewHolder.deliveredIndicator, null);
+            }
+
+            if (viewHolder.groupAckContainer != null) {
+                stateBitmapUtil.setGroupAckCount(messageModel, viewHolder);
+            }
+
+            if (viewHolder.editedText != null) {
+                viewHolder.editedText.setVisibility(messageModel.getEditedAt() != null ? View.VISIBLE : View.GONE);
+            }
+
+            if (viewHolder.controller != null) {
+                viewHolder.controller.setIsUsedForOutboxMessage(getMessageModel().isOutbox());
+            }
+        }
+    }
+
+    public Spannable highlightMatches(CharSequence fullText, String filterText) {
+        return TextUtil.highlightMatches(getContext(), fullText, filterText, true, false);
+    }
+
+    CharSequence formatTextString(@Nullable String string, String filterString) {
+        return formatTextString(string, filterString, -1);
+    }
+
+    CharSequence formatTextString(@Nullable String string, String filterString, int maxLength) {
+        if (TextUtils.isEmpty(string)) {
+            return "";
+        }
+
+        if (maxLength > 0 && string.length() > maxLength) {
+            return highlightMatches(string.substring(0, maxLength - 1), filterString);
+        }
+        return highlightMatches(string, filterString);
+    }
+
+    protected void configureChatMessage(final ComposeMessageHolder holder, final int position) {
+        if (holder.attachmentImage instanceof ShapeableImageView) {
+            ShapeAppearanceModel shapeAppearanceModel = new ShapeAppearanceModel.Builder()
+                .setAllCornerSizes(ImageViewUtil.getCornerRadius(getContext()))
+                .build();
+            ((ShapeableImageView) holder.attachmentImage).setShapeAppearanceModel(shapeAppearanceModel);
+        }
+    }
+
+    protected void setDatePrefix(String prefix) {
+        datePrefix = prefix;
+    }
+
+    protected void setDuration(long durationS) {
+        this.durationS = durationS;
+    }
+
+    protected MessageService getMessageService() {
+        return helper.getMessageService();
+    }
+
+    protected MessagePlayerService getMessagePlayerService() {
+        return helper.getMessagePlayerService();
+    }
+
+    protected FileService getFileService() {
+        return helper.getFileService();
+    }
+
+    protected int getThumbnailWidth() {
+        return helper.getThumbnailWidth();
+    }
+
+    protected ThumbnailCache getThumbnailCache() {
+        return helper.getThumbnailCache();
+    }
+
+    protected AbstractMessageModel getMessageModel() {
+        return messageModel;
+    }
+
+    protected PreferenceService getPreferenceService() {
+        return helper.getPreferenceService();
+    }
+
+    protected UserService getUserService() {
+        return helper.getUserService();
+    }
+
+    protected ContactService getContactService() {
+        return helper.getContactService();
+    }
+
+    protected void setOnClickListener(final View.OnClickListener onViewClickListener, View view) {
+        if (view != null) {
+            view.setOnClickListener(v -> {
+                if (onViewClickListener != null && !actionModeStatus.getActionModeEnabled()) {
+                    // do not propagate click if actionMode (selection mode) is enabled in parent
+                    onViewClickListener.onClick(v);
+                }
+                if (onClickElement != null) {
+                    //propagate event to parents
+                    onClickElement.onClick(getMessageModel());
+                }
+            });
 
 //			propagate long click listener
-			view.setOnLongClickListener(v -> {
-				if (onLongClickElement != null) {
-					onLongClickElement.onLongClick(getMessageModel());
-				}
-				return false;
-			});
+            view.setOnLongClickListener(v -> {
+                if (onLongClickElement != null) {
+                    onLongClickElement.onLongClick(getMessageModel());
+                }
+                return false;
+            });
 
 //			propagate touch listener
-			view.setOnTouchListener(new View.OnTouchListener() {
-				@Override
-				public boolean onTouch(View arg0, MotionEvent event) {
-					if (onTouchElement != null) {
-						return onTouchElement.onTouch(event, getMessageModel());
-					}
-					return false;
-				}
-			});
-		}
-	}
-
-	void setStickerBackground(ComposeMessageHolder holder) {
-		holder.messageBlockView.setCardBackgroundColor(AppCompatResources.getColorStateList(getContext(), R.color.bubble_sticker_colorstatelist));
-	}
-
-	void setDefaultBackground(ComposeMessageHolder holder) {
-		if (holder.messageBlockView.getCardBackgroundColor().getDefaultColor() == Color.TRANSPARENT) {
-			int colorStateListRes;
-
-			if (getMessageModel().isOutbox() && !(getMessageModel() instanceof DistributionListMessageModel)) {
-				// outgoing
-				colorStateListRes = R.color.bubble_send_colorstatelist;
-			} else {
-				// incoming
-				colorStateListRes = R.color.bubble_receive_colorstatelist;
-			}
-			holder.messageBlockView.setCardBackgroundColor(AppCompatResources.getColorStateList(getContext(), colorStateListRes));
-
-			logger.debug("*** setDefaultBackground");
-		}
-	}
-
-	/**
-	 * Set whether this message should be displayed as the continuation of a previous message by the same sender
-	 * @param grouped If this is a grouped message, following another message by the same sender
-	 */
-	public void setGroupedMessage(boolean grouped) {
-		isGroupedMessage = grouped;
-	}
-
-	/**
-	 * Setup "Tap to resend" UI
-	 * @param holder ComposeMessageHolder
-	 */
-	protected void setupResendStatus(ComposeMessageHolder holder) {
-		if (holder.tapToResend != null) {
-			if (getMessageModel() != null &&
-				getMessageModel().isOutbox() &&
-				(getMessageModel().getState() == MessageState.FS_KEY_MISMATCH ||
-				getMessageModel().getState() == MessageState.SENDFAILED)) {
-				holder.tapToResend.setVisibility(View.VISIBLE);
-				holder.dateView.setVisibility(View.GONE);
-			} else {
-				holder.tapToResend.setVisibility(View.GONE);
-				holder.dateView.setVisibility(View.VISIBLE);
-			}
-		}
-	}
-
-	protected void configureBodyText(@NonNull ComposeMessageHolder holder, @Nullable String caption) {
-		if (!TestUtil.isEmptyOrNull(caption)) {
-			holder.bodyTextView.setText(formatTextString(caption, filterString));
-
-			LinkifyUtil.getInstance().linkify(
-				(ComposeMessageFragment) helper.getFragment(),
-				holder.bodyTextView,
-				getMessageModel(),
-				true,
-				actionModeStatus.getActionModeEnabled(),
-				onClickElement);
-
-			showHide(holder.bodyTextView, true);
-		} else {
-			showHide(holder.bodyTextView, false);
-		}
-	}
-
-	protected void propagateControllerRetryClickToParent() {
-		if (
-			getMessageModel().getState() == MessageState.FS_KEY_MISMATCH ||
-				getMessageModel().getState() == MessageState.SENDFAILED
-		) {
-			propagateControllerClickToParent();
-		}
-	}
-
-	protected void propagateControllerClickToParent() {
-		if (onClickElement != null) {
-			onClickElement.onClick(getMessageModel());
-		}
-	}
+            view.setOnTouchListener(new View.OnTouchListener() {
+                @Override
+                public boolean onTouch(View arg0, MotionEvent event) {
+                    if (onTouchElement != null) {
+                        return onTouchElement.onTouch(event, getMessageModel());
+                    }
+                    return false;
+                }
+            });
+        }
+    }
+
+    void setStickerBackground(ComposeMessageHolder holder) {
+        holder.messageBlockView.setCardBackgroundColor(AppCompatResources.getColorStateList(getContext(), R.color.bubble_sticker_colorstatelist));
+    }
+
+    void setDefaultBackground(ComposeMessageHolder holder) {
+        if (holder.messageBlockView.getCardBackgroundColor().getDefaultColor() == Color.TRANSPARENT) {
+            int colorStateListRes;
+
+            if (getMessageModel().isOutbox() && !(getMessageModel() instanceof DistributionListMessageModel)) {
+                // outgoing
+                colorStateListRes = R.color.bubble_send_colorstatelist;
+            } else {
+                // incoming
+                colorStateListRes = R.color.bubble_receive_colorstatelist;
+            }
+            holder.messageBlockView.setCardBackgroundColor(AppCompatResources.getColorStateList(getContext(), colorStateListRes));
+
+            logger.debug("*** setDefaultBackground");
+        }
+    }
+
+    /**
+     * Set whether this message should be displayed as the continuation of a previous message by the same sender
+     *
+     * @param grouped If this is a grouped message, following another message by the same sender
+     */
+    public void setGroupedMessage(boolean grouped) {
+        isGroupedMessage = grouped;
+    }
+
+    /**
+     * Setup "Tap to resend" UI
+     *
+     * @param holder ComposeMessageHolder
+     */
+    protected void setupResendStatus(ComposeMessageHolder holder) {
+        if (holder.tapToResend != null) {
+            if (getMessageModel() != null &&
+                getMessageModel().isOutbox() &&
+                (getMessageModel().getState() == MessageState.FS_KEY_MISMATCH ||
+                    getMessageModel().getState() == MessageState.SENDFAILED)) {
+                holder.tapToResend.setVisibility(View.VISIBLE);
+                holder.dateView.setVisibility(View.GONE);
+            } else {
+                holder.tapToResend.setVisibility(View.GONE);
+                holder.dateView.setVisibility(View.VISIBLE);
+            }
+        }
+    }
+
+    protected void configureBodyText(@NonNull ComposeMessageHolder holder, @Nullable String caption) {
+        if (!TestUtil.isEmptyOrNull(caption)) {
+            holder.bodyTextView.setText(formatTextString(caption, filterString));
+
+            LinkifyUtil.getInstance().linkify(
+                (ComposeMessageFragment) helper.getFragment(),
+                holder.bodyTextView,
+                getMessageModel(),
+                true,
+                actionModeStatus.getActionModeEnabled(),
+                onClickElement);
+
+            showHide(holder.bodyTextView, true);
+        } else {
+            showHide(holder.bodyTextView, false);
+        }
+    }
+
+    protected void propagateControllerRetryClickToParent() {
+        if (
+            getMessageModel().getState() == MessageState.FS_KEY_MISMATCH ||
+                getMessageModel().getState() == MessageState.SENDFAILED
+        ) {
+            propagateControllerClickToParent();
+        }
+    }
+
+    protected void propagateControllerClickToParent() {
+        if (onClickElement != null) {
+            onClickElement.onClick(getMessageModel());
+        }
+    }
 }

+ 14 - 0
app/src/main/java/ch/threema/app/adapters/decorators/DateSeparatorChatAdapterDecorator.java

@@ -22,9 +22,12 @@
 package ch.threema.app.adapters.decorators;
 
 import android.content.Context;
+import android.content.res.ColorStateList;
 
 import java.util.Date;
 
+import androidx.annotation.NonNull;
+import ch.threema.app.R;
 import ch.threema.app.ui.listitemholder.ComposeMessageHolder;
 import ch.threema.app.utils.LocaleUtil;
 import ch.threema.storage.models.AbstractMessageModel;
@@ -34,6 +37,17 @@ public class DateSeparatorChatAdapterDecorator extends ChatAdapterDecorator {
 		super(context, messageModel, helper);
 	}
 
+    @Override
+    protected void applyContentColor(
+        final @NonNull ComposeMessageHolder viewHolder,
+        final @NonNull ColorStateList contentColor
+    ) {
+        super.applyContentColor(viewHolder, contentColor);
+        viewHolder.bodyTextView.setTextColor(
+            getContext().getResources().getColor(R.color.date_separator_text_color)
+        );
+    }
+
 	@Override
 	protected void configureChatMessage(final ComposeMessageHolder holder, final int position) {
 		Date date = this.getMessageModel().getCreatedAt();

+ 1 - 1
app/src/main/java/ch/threema/app/adapters/decorators/FileChatAdapterDecorator.java

@@ -309,7 +309,7 @@ public class FileChatAdapterDecorator extends ChatAdapterDecorator {
 				}
 			} else {
 				if (holder.controller != null) {
-					holder.controller.setImageResource(IconUtil.getMimeIcon(fileData.getMimeType()));
+					holder.controller.setIconResource(IconUtil.getMimeIcon(fileData.getMimeType()));
 				}
 			}
 

+ 1 - 1
app/src/main/java/ch/threema/app/adapters/decorators/LocationChatAdapterDecorator.java

@@ -106,7 +106,7 @@ public class LocationChatAdapterDecorator extends ChatAdapterDecorator {
 
 		if (position == holder.position) {
 			holder.controller.setBackgroundImage(null);
-			holder.controller.setImageResource(R.drawable.ic_map_marker_outline);
+			holder.controller.setIconResource(R.drawable.ic_map_marker_outline);
 		}
 
 		RuntimeUtil.runOnUiThread(() -> setupResendStatus(holder));

+ 1 - 1
app/src/main/java/ch/threema/app/adapters/decorators/VoipStatusDataChatAdapterDecorator.java

@@ -49,7 +49,7 @@ public class VoipStatusDataChatAdapterDecorator extends ChatAdapterDecorator {
 	protected void configureChatMessage(final ComposeMessageHolder holder, final int position) {
 		if (holder.controller != null) {
 			holder.controller.setClickable(false);
-			holder.controller.setImageResource(R.drawable.ic_phone_locked_outline);
+			holder.controller.setIconResource(R.drawable.ic_phone_locked_outline);
 		}
 
 		if(holder.bodyTextView != null) {

+ 6 - 0
app/src/main/java/ch/threema/app/compose/common/interop/InteropEmojiConversationTextView.kt

@@ -28,6 +28,7 @@ import androidx.annotation.StyleRes
 import androidx.appcompat.view.ContextThemeWrapper
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -35,6 +36,9 @@ import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.isSpecified
+import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.input.pointer.pointerInteropFilter
 import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.semantics
@@ -50,6 +54,7 @@ import ch.threema.app.utils.LinkifyUtil
 fun InteropEmojiConversationTextView(
     text: String,
     @StyleRes textAppearanceRes: Int,
+    contentColor: Color,
     shouldMarkupText: Boolean = true,
     isTextSelectable: Boolean = false,
     maxLines: Int = Integer.MAX_VALUE,
@@ -71,6 +76,7 @@ fun InteropEmojiConversationTextView(
             factory = { context ->
                 val textView = EmojiConversationTextView(ContextThemeWrapper(context, R.style.AppBaseTheme))
                 TextViewCompat.setTextAppearance(textView, textAppearanceRes)
+                textView.setTextColor(contentColor.toArgb())
 
                 textView.apply {
                     setMaxLines(maxLines)

+ 3 - 1
app/src/main/java/ch/threema/app/compose/message/DeliveryIcon.kt

@@ -28,6 +28,7 @@ import androidx.compose.material3.Icon
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
@@ -36,11 +37,12 @@ import androidx.compose.ui.unit.dp
 fun DeliveryIndicator(
     @DrawableRes deliveryIconRes: Int,
     @StringRes deliveryIconContentDescriptionRes: Int,
+    tintColor: Color?
 ) {
     Icon(
         modifier = Modifier.size(18.dp),
         painter = painterResource(deliveryIconRes),
-        tint = MaterialTheme.colorScheme.onSurface,
+        tint = tintColor ?: MaterialTheme.colorScheme.onSurface,
         contentDescription = stringResource(deliveryIconContentDescriptionRes)
     )
 }

+ 35 - 11
app/src/main/java/ch/threema/app/compose/message/MessageBubble.kt

@@ -40,6 +40,7 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.semantics.contentDescription
@@ -73,9 +74,18 @@ fun MessageBubble(
     isTextSelectable: Boolean = false,
     onClick: (() -> Unit)? = null,
     @SuppressLint("ComposableLambdaParameterNaming")
-    footerContent: @Composable (() -> Unit)? = null
+    footerContent: @Composable ((contentColor: Color) -> Unit)? = null
 ) {
-    val bubbleColor = if (isOutbox) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.customColorScheme.messageBubbleContainerReceive
+    val bubbleColor: Color = if (isOutbox) {
+        MaterialTheme.colorScheme.secondaryContainer
+    } else {
+        MaterialTheme.customColorScheme.messageBubbleContainerReceive
+    }
+    val contentColor = if (isOutbox) {
+        MaterialTheme.colorScheme.onSecondaryContainer
+    } else {
+        MaterialTheme.colorScheme.onBackground
+    }
     Column(
         modifier = modifier
             .background(
@@ -89,12 +99,13 @@ fun MessageBubble(
         InteropEmojiConversationTextView(
             text = text,
             textAppearanceRes = textAppearanceRes,
+            contentColor = contentColor,
             shouldMarkupText = shouldMarkupText,
             textSelectionCallback = textSelectionCallback,
             isTextSelectable = isTextSelectable,
         )
         Spacer(modifier = Modifier.size(4.dp))
-        footerContent?.invoke()
+        footerContent?.invoke(contentColor)
     }
 }
 
@@ -135,13 +146,14 @@ fun CompleteMessageBubble(
             } else {
                 R.style.Threema_Bubble_Text_Body
             },
-            footerContent = {
+            footerContent = { contentColor: Color ->
                 MessageBubbleFooter(
                     shouldShowEditedLabel = message.editedAt != null,
                     date = message.createdAt,
                     isOutbox = message.isOutbox,
                     deliveryIconRes = message.deliveryIconRes,
                     deliveryIconContentDescriptionRes = message.deliveryIconContentDescriptionRes,
+                    contentColor = contentColor,
                     ackUiModel = message.ackUiModel
                 )
             }
@@ -160,8 +172,13 @@ fun DeletedMessageBubble(
         textAppearanceRes = R.style.Threema_Bubble_Text_Body_Deleted,
         isOutbox = isOutbox,
         onClick = onClick,
-        footerContent = {
-            MessageBubbleFooter(shouldShowEditedLabel = false, isOutbox = isOutbox, date = date)
+        footerContent = { contentColor ->
+            MessageBubbleFooter(
+                shouldShowEditedLabel = false,
+                isOutbox = isOutbox,
+                date = date,
+                contentColor = contentColor
+            )
         }
     )
 }
@@ -173,6 +190,7 @@ fun MessageBubbleFooter(
     isOutbox: Boolean,
     @DrawableRes deliveryIconRes: Int? = null,
     @StringRes deliveryIconContentDescriptionRes: Int? = null,
+    contentColor: Color,
     ackUiModel: AckUiModel? = null
 ) {
     Row(
@@ -185,13 +203,16 @@ fun MessageBubbleFooter(
             ThemedText(
                 modifier = stringResource(R.string.cd_edited).let { Modifier.semantics { contentDescription = it } },
                 text = stringResource(R.string.edited),
-                style = AppTypography.bodySmall
+                style = AppTypography.bodySmall,
+                color = contentColor
             )
         }
         Spacer(modifier = Modifier.weight(1f))
         if (ackUiModel != null && !isOutbox) {
             Spacer(modifier = Modifier.size(8.dp))
-            MessageStateIndicator(ackUiModel = ackUiModel)
+            MessageStateIndicator(
+                ackUiModel = ackUiModel,
+            )
         }
         date?.let {
             Spacer(modifier = Modifier.size(4.dp))
@@ -199,7 +220,8 @@ fun MessageBubbleFooter(
             ThemedText(
                 modifier = stringResource(R.string.cd_created_at).let { Modifier.semantics { contentDescription = it.format(formattedDate) } },
                 text = formattedDate,
-                style = AppTypography.bodySmall
+                style = AppTypography.bodySmall,
+                color = contentColor
             )
         }
         if ((ackUiModel != null || deliveryIconRes != null) && isOutbox) {
@@ -207,7 +229,8 @@ fun MessageBubbleFooter(
             MessageStateIndicator(
                 ackUiModel = ackUiModel,
                 deliveryIconRes = deliveryIconRes,
-                deliveryIconContentDescriptionRes = deliveryIconContentDescriptionRes
+                deliveryIconContentDescriptionRes = deliveryIconContentDescriptionRes,
+                deliveryIndicatorTintColor = contentColor
             )
         }
     }
@@ -220,12 +243,13 @@ private fun MessageBubblePreview() {
         text = "Lorem ipsum *dolor sit amet*, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam",
         isOutbox = true,
         shouldMarkupText = true,
-        footerContent = {
+        footerContent = { contentColor: Color ->
             MessageBubbleFooter(
                 shouldShowEditedLabel = true,
                 date = Date(),
                 isOutbox = true,
                 deliveryIconRes = R.drawable.ic_mark_read,
+                contentColor = contentColor,
                 ackUiModel = GroupAckUiModel(GroupAckDecState(2, UserReaction.REACTED), GroupAckDecState(1, UserReaction.NONE)),
             )
         }

+ 7 - 1
app/src/main/java/ch/threema/app/compose/message/MessageStateIndicator.kt

@@ -24,6 +24,7 @@ package ch.threema.app.compose.message
 import androidx.annotation.DrawableRes
 import androidx.annotation.StringRes
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
 import ch.threema.app.activities.AckUiModel
 import ch.threema.app.activities.ContactAckUiModel
 import ch.threema.app.activities.GroupAckUiModel
@@ -33,6 +34,7 @@ fun MessageStateIndicator(
     ackUiModel: AckUiModel? = null,
     @DrawableRes deliveryIconRes: Int? = null,
     @StringRes deliveryIconContentDescriptionRes: Int? = null,
+    deliveryIndicatorTintColor: Color? = null
 ) {
     when (ackUiModel) {
         is ContactAckUiModel -> {
@@ -44,7 +46,11 @@ fun MessageStateIndicator(
 
         null -> {
             if (deliveryIconRes != null && deliveryIconContentDescriptionRes != null) {
-                DeliveryIndicator(deliveryIconRes = deliveryIconRes, deliveryIconContentDescriptionRes = deliveryIconContentDescriptionRes)
+                DeliveryIndicator(
+                    deliveryIconRes = deliveryIconRes,
+                    deliveryIconContentDescriptionRes = deliveryIconContentDescriptionRes,
+                    tintColor = deliveryIndicatorTintColor
+                )
             }
         }
     }

+ 18 - 15
app/src/main/java/ch/threema/app/fragments/ComposeMessageFragment.java

@@ -33,6 +33,7 @@ import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
+import android.graphics.Color;
 import android.graphics.Paint;
 import android.graphics.Typeface;
 import android.media.AudioManager;
@@ -415,7 +416,7 @@ public class ComposeMessageFragment extends Fragment implements
 	private ContactService contactService;
 	private MessageService messageService;
 	private NotificationService notificationService;
-	private IdListService blackListIdentityService;
+	private IdListService blockedContactsService;
 	private ConversationService conversationService;
 	private DeviceService deviceService;
 	private WallpaperService wallpaperService;
@@ -2040,9 +2041,11 @@ public class ComposeMessageFragment extends Fragment implements
 		if (TestUtil.isBlankOrNull(newMessageText) || newMessageText.equals(oldMessageText)) {
 			sendEditMessageButton.setEnabled(false);
 			sendEditMessageButton.setBackground(ContextCompat.getDrawable(requireContext(), R.drawable.ic_circle_send_disabled));
+            sendEditMessageButton.setColorFilter(ConfigUtils.getColorFromAttribute(getContext(), R.attr.colorOnSurfaceVariant));
 		} else {
 			sendEditMessageButton.setEnabled(true);
 			sendEditMessageButton.setBackground(ContextCompat.getDrawable(requireContext(), R.drawable.ic_circle_send));
+            sendEditMessageButton.setColorFilter(ConfigUtils.getColorFromAttribute(getContext(), R.attr.colorOnPrimaryContainer));
 		}
 	}
 
@@ -2620,7 +2623,7 @@ public class ComposeMessageFragment extends Fragment implements
 		}
 
 		// Exclude blocked contacts
-		if (blackListIdentityService.has(contactModel.getIdentity())) {
+		if (blockedContactsService.has(contactModel.getIdentity())) {
 			return false;
 		}
 
@@ -4118,7 +4121,7 @@ public class ComposeMessageFragment extends Fragment implements
 
 		super.onCreateOptionsMenu(menu, inflater);
 
-		ConfigUtils.addIconsToOverflowMenu(getContext(), menu);
+		ConfigUtils.addIconsToOverflowMenu(menu);
 	}
 
 	@Override
@@ -4203,7 +4206,7 @@ public class ComposeMessageFragment extends Fragment implements
 				}
 				Context context = getContext();
 				if (context != null) {
-					ConfigUtils.tintMenuItem(context, showOpenBallotWindowMenuItem, R.attr.colorOnSurface);
+					ConfigUtils.tintMenuIcon(context, showOpenBallotWindowMenuItem, R.attr.colorOnSurface);
 				}
 			}
 		}.execute();
@@ -4285,8 +4288,8 @@ public class ComposeMessageFragment extends Fragment implements
 			// do not update if no longer attached to activity
 			return;
 		}
-		if (TestUtil.required(this.blockMenuItem, this.blackListIdentityService, this.contactModel)) {
-			boolean state = this.blackListIdentityService.has(this.contactModel.getIdentity());
+		if (TestUtil.required(this.blockMenuItem, this.blockedContactsService, this.contactModel)) {
+			boolean state = this.blockedContactsService.has(this.contactModel.getIdentity());
 			this.blockMenuItem.setTitle(state ? getString(R.string.unblock_contact) : getString(R.string.block_contact));
 			this.blockMenuItem.setShowAsAction(state ? MenuItem.SHOW_AS_ACTION_ALWAYS : MenuItem.SHOW_AS_ACTION_NEVER);
 			this.mutedMenuItem.setShowAsAction(state ? MenuItem.SHOW_AS_ACTION_NEVER : MenuItem.SHOW_AS_ACTION_IF_ROOM);
@@ -4304,7 +4307,7 @@ public class ComposeMessageFragment extends Fragment implements
 			if (isGroupChat) {
 				updateGroupCallMenuItem();
 			} else if (callItem != null) {
-				if (ContactUtil.canReceiveVoipMessages(contactModel, blackListIdentityService) && ConfigUtils.isCallsEnabled()) {
+				if (ContactUtil.canReceiveVoipMessages(contactModel, blockedContactsService) && ConfigUtils.isCallsEnabled()) {
 					logger.debug("updateVoipMenu newState " + newState);
 					callItem.setIcon(R.drawable.ic_phone_locked_outline);
 					callItem.setTitle(R.string.threema_call);
@@ -4409,8 +4412,8 @@ public class ComposeMessageFragment extends Fragment implements
 				activity.startActivity(intent);
 			}
 		} else if (id == R.id.menu_block_contact) {
-			if (this.blackListIdentityService.has(contactModel.getIdentity())) {
-				this.blackListIdentityService.toggle(activity, contactModel);
+			if (this.blockedContactsService.has(contactModel.getIdentity())) {
+				this.blockedContactsService.toggle(activity, contactModel);
 				updateBlockMenu();
 			} else {
 				GenericAlertDialog.newInstance(R.string.block_contact, R.string.really_block_contact, R.string.yes, R.string.no).setTargetFragment(this).show(getFragmentManager(), DIALOG_TAG_CONFIRM_BLOCK);
@@ -4513,7 +4516,7 @@ public class ComposeMessageFragment extends Fragment implements
 	private void createShortcut() {
 		if (!this.isGroupChat &&
 			!this.isDistributionListChat &&
-			ContactUtil.canReceiveVoipMessages(contactModel, blackListIdentityService) &&
+			ContactUtil.canReceiveVoipMessages(contactModel, blockedContactsService) &&
 			ConfigUtils.isCallsEnabled()) {
 							ArrayList<SelectorDialogItem> items = new ArrayList<>();
 				items.add(new SelectorDialogItem(getString(R.string.prefs_header_chat), R.drawable.ic_outline_chat_bubble_outline));
@@ -4747,7 +4750,7 @@ public class ComposeMessageFragment extends Fragment implements
 				inflater.inflate(R.menu.action_compose_message, menu);
 			}
 
-			ConfigUtils.addIconsToOverflowMenu(null, menu);
+			ConfigUtils.addIconsToOverflowMenu(menu);
 
 			forwardItem = menu.findItem(R.id.menu_message_forward);
 			saveItem = menu.findItem(R.id.menu_message_save);
@@ -5336,7 +5339,7 @@ public class ComposeMessageFragment extends Fragment implements
 				this.notificationService,
 				this.distributionListService,
 				this.messagePlayerService,
-				this.blackListIdentityService,
+				this.blockedContactsService,
 				this.ballotService,
 				this.conversationService,
 				this.deviceService,
@@ -5361,7 +5364,7 @@ public class ComposeMessageFragment extends Fragment implements
 				this.notificationService = serviceManager.getNotificationService();
 				this.distributionListService = serviceManager.getDistributionListService();
 				this.messagePlayerService = serviceManager.getMessagePlayerService();
-				this.blackListIdentityService = serviceManager.getBlackListService();
+				this.blockedContactsService = serviceManager.getBlockedContactsService();
 				this.ballotService = serviceManager.getBallotService();
 				this.databaseServiceNew = serviceManager.getDatabaseServiceNew();
 				this.conversationService = serviceManager.getConversationService();
@@ -5404,7 +5407,7 @@ public class ComposeMessageFragment extends Fragment implements
 				emptyChat();
 				break;
 			case DIALOG_TAG_CONFIRM_BLOCK:
-				blackListIdentityService.toggle(activity, contactModel);
+				blockedContactsService.toggle(activity, contactModel);
 				updateBlockMenu();
 				break;
 			case DIALOG_TAG_CONFIRM_LINK:
@@ -5599,7 +5602,7 @@ public class ComposeMessageFragment extends Fragment implements
 
 				final String spammerIdentity = spammerContactModel.getIdentity();
 				if (block) {
-					blackListIdentityService.add(spammerIdentity);
+					blockedContactsService.add(spammerIdentity);
 					ThreemaApplication.requireServiceManager().getExcludedSyncIdentitiesService().add(spammerIdentity);
 
 					if (messageReceiver != null) {

+ 5 - 5
app/src/main/java/ch/threema/app/fragments/ContactsSectionFragment.java

@@ -662,7 +662,7 @@ public class ContactsSectionFragment
 							contactModels,
 							contactService,
 							serviceManager.getPreferenceService(),
-							serviceManager.getBlackListService(),
+							serviceManager.getBlockedContactsService(),
 							ContactsSectionFragment.this,
 							Glide.with(getContext())
 						);
@@ -846,7 +846,7 @@ public class ContactsSectionFragment
 					mode.getMenuInflater().inflate(R.menu.action_contacts_section, menu);
 					actionMode = mode;
 
-					ConfigUtils.tintMenu(menu, ConfigUtils.getColorFromAttribute(getContext(), R.attr.colorPrimary));
+					ConfigUtils.tintMenuIcons(menu, ConfigUtils.getColorFromAttribute(getContext(), R.attr.colorPrimary));
 
 					return true;
 				}
@@ -1196,7 +1196,7 @@ public class ContactsSectionFragment
 				}
 			}
 
-			if (serviceManager.getBlackListService().has(contactModel.getIdentity())) {
+			if (serviceManager.getBlockedContactsService().has(contactModel.getIdentity())) {
 				items.add(new SelectorDialogItem(getString(R.string.unblock_contact), R.drawable.ic_block));
 			} else {
 				items.add(new SelectorDialogItem(getString(R.string.block_contact), R.drawable.ic_block));
@@ -1429,7 +1429,7 @@ public class ContactsSectionFragment
 				sdialog.show(getParentFragmentManager(), DIALOG_TAG_REPORT_SPAM);
 				break;
 			case SELECTOR_TAG_BLOCK:
-				serviceManager.getBlackListService().toggle(getActivity(), contactModel);
+				serviceManager.getBlockedContactsService().toggle(getActivity(), contactModel);
 				break;
 			case SELECTOR_TAG_DELETE:
 				deleteContacts(Set.of(contactModel));
@@ -1461,7 +1461,7 @@ public class ContactsSectionFragment
 
 						final String spammerIdentity = contactModel.getIdentity();
 						if (checked) {
-							ThreemaApplication.requireServiceManager().getBlackListService().add(spammerIdentity);
+							ThreemaApplication.requireServiceManager().getBlockedContactsService().add(spammerIdentity);
 							ThreemaApplication.requireServiceManager().getExcludedSyncIdentitiesService().add(spammerIdentity);
 
 							try {

+ 2 - 2
app/src/main/java/ch/threema/app/fragments/MemberListFragment.java

@@ -69,7 +69,7 @@ public abstract class MemberListFragment extends ListFragment implements FilterR
 	protected DistributionListService distributionListService;
 	protected ConversationService conversationService;
 	protected PreferenceService preferenceService;
-	protected IdListService blacklistService;
+	protected IdListService blockedContactsService;
 	protected DeadlineListService hiddenChatsListService;
 	protected Activity activity;
 	protected Parcelable listInstanceState;
@@ -92,7 +92,7 @@ public abstract class MemberListFragment extends ListFragment implements FilterR
 			contactService = serviceManager.getContactService();
 			groupService = serviceManager.getGroupService();
 			distributionListService = serviceManager.getDistributionListService();
-			blacklistService = serviceManager.getBlackListService();
+			blockedContactsService = serviceManager.getBlockedContactsService();
 			conversationService = serviceManager.getConversationService();
 			preferenceService = serviceManager.getPreferenceService();
 			hiddenChatsListService = serviceManager.getHiddenChatsListService();

+ 2 - 2
app/src/main/java/ch/threema/app/fragments/RecipientListFragment.java

@@ -95,7 +95,7 @@ public abstract class RecipientListFragment extends ListFragment implements List
 	protected DistributionListService distributionListService;
 	protected ConversationService conversationService;
 	protected PreferenceService preferenceService;
-	protected IdListService blacklistService;
+	protected IdListService blockedContactsService;
 	protected DeadlineListService hiddenChatsListService;
 	protected FragmentActivity activity;
 	protected Parcelable listInstanceState;
@@ -119,7 +119,7 @@ public abstract class RecipientListFragment extends ListFragment implements List
 			contactService = serviceManager.getContactService();
 			groupService = serviceManager.getGroupService();
 			distributionListService = serviceManager.getDistributionListService();
-			blacklistService = serviceManager.getBlackListService();
+			blockedContactsService = serviceManager.getBlockedContactsService();
 			conversationService = serviceManager.getConversationService();
 			preferenceService = serviceManager.getPreferenceService();
 			hiddenChatsListService = serviceManager.getHiddenChatsListService();

+ 1 - 1
app/src/main/java/ch/threema/app/fragments/UserListFragment.java

@@ -102,7 +102,7 @@ public class UserListFragment extends RecipientListFragment {
 					null,
 					checkedItemPositions,
 					contactService,
-					blacklistService,
+					blockedContactsService,
 					hiddenChatsListService,
 					preferenceService,
 					UserListFragment.this,

+ 1 - 1
app/src/main/java/ch/threema/app/fragments/UserMemberListFragment.java

@@ -141,7 +141,7 @@ public class UserMemberListFragment extends MemberListFragment {
 					preselectedIdentities,
 					checkedItemPositions,
 					contactService,
-					blacklistService,
+					blockedContactsService,
 					hiddenChatsListService,
 					preferenceService,
 					UserMemberListFragment.this,

+ 1 - 1
app/src/main/java/ch/threema/app/fragments/WorkUserListFragment.java

@@ -173,7 +173,7 @@ public class WorkUserListFragment extends RecipientListFragment {
 					null,
 					checkedItemPositions,
 					contactService,
-					blacklistService,
+					blockedContactsService,
 					hiddenChatsListService,
 					preferenceService,
 					WorkUserListFragment.this,

+ 1 - 1
app/src/main/java/ch/threema/app/fragments/WorkUserMemberListFragment.java

@@ -130,7 +130,7 @@ public class WorkUserMemberListFragment extends MemberListFragment {
 						preselectedIdentities,
 						checkedItemPositions,
 						contactService,
-						blacklistService,
+						blockedContactsService,
 						hiddenChatsListService,
 						preferenceService,
 						WorkUserMemberListFragment.this,

+ 5 - 1
app/src/main/java/ch/threema/app/globalsearch/GlobalSearchActivity.kt

@@ -178,7 +178,11 @@ class GlobalSearchActivity : ThreemaToolbarActivity(), SearchView.OnQueryTextLis
         recyclerView.layoutManager = LinearLayoutManager(this)
         recyclerView.itemAnimator = DefaultItemAnimator()
         val emptyView = EmptyView(this, ConfigUtils.getActionBarSize(this))
-        emptyView.setup(R.string.global_search_empty_view_text, R.drawable.ic_search_outline)
+        emptyView.setup(
+            R.string.global_search_empty_view_text,
+            R.drawable.ic_search_outline,
+            ConfigUtils.getColorFromAttribute(this, R.attr.colorOnBackground)
+        )
         (recyclerView.parent.parent as ViewGroup).addView(emptyView)
         recyclerView.emptyView = emptyView
         emptyView.setLoading(true)

+ 453 - 403
app/src/main/java/ch/threema/app/globalsearch/GlobalSearchAdapter.java

@@ -67,6 +67,7 @@ import ch.threema.app.ui.AvatarListItemUtil;
 import ch.threema.app.ui.AvatarView;
 import ch.threema.app.ui.CheckableRelativeLayout;
 import ch.threema.app.ui.listitemholder.AvatarListItemHolder;
+import ch.threema.app.utils.ColorUtil;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.IconUtil;
 import ch.threema.app.utils.LocaleUtil;
@@ -86,407 +87,456 @@ import ch.threema.storage.models.data.MessageContentsType;
 import ch.threema.storage.models.data.media.BallotDataModel;
 
 public class GlobalSearchAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
-	private static final Logger logger = LoggingUtil.getThreemaLogger("GlobalSearchAdapter");
-	private static final String FLOW_CHARACTER = "\u25BA\uFE0E";
-
-	private GroupService groupService;
-	private ContactService contactService;
-	private BallotService ballotService;
-	private DeadlineListService hiddenChatsListService;
-
-	private final Context context;
-	private OnClickItemListener onClickItemListener;
-	private final SparseBooleanArray checkedItems = new SparseBooleanArray();
-	private String queryString;
-	private final int snippetThreshold;
-	private List<AbstractMessageModel> messageModels; // Cached copy of AbstractMessageModels
-	private final @ColorInt int foregroundColor;
-	private final @LayoutRes int itemLayout;
-	private final ColorStateList colorStateListSend, colorStateListReceive;
-	private final @NonNull RequestManager requestManager;
-
-	private static class ItemHolder extends RecyclerView.ViewHolder {
-		private final TextView titleView;
-		private final TextView dateView;
-		private final TextView snippetView;
-		@Nullable private final TextView deletedPlaceholder; // will be null for layouts other than item_starred_messages
-		private final ImageView thumbnailView;
-		private final MaterialCardView messageBlock;
-		AvatarListItemHolder avatarListItemHolder;
-
-		private ItemHolder(final View itemView) {
-			super(itemView);
-
-			titleView = itemView.findViewById(R.id.name);
-			dateView = itemView.findViewById(R.id.date);
-			snippetView = itemView.findViewById(R.id.snippet);
-			deletedPlaceholder = itemView.findViewById(R.id.deleted_placeholder);
-			AvatarView avatarView = itemView.findViewById(R.id.avatar_view);
-			thumbnailView = itemView.findViewById(R.id.thumbnail_view);
-			messageBlock = itemView.findViewById(R.id.message_block);
-
-			avatarListItemHolder = new AvatarListItemHolder();
-			avatarListItemHolder.avatarView = avatarView;
-			avatarListItemHolder.avatarLoadingAsyncTask = null;
-		}
-	}
-
-	public GlobalSearchAdapter(
-		Context context,
-		@NonNull RequestManager requestManager,
-		@LayoutRes int itemLayout,
-		int snippetThreshold
-	) {
-		this.context = context;
-		this.requestManager = requestManager;
-		this.itemLayout = itemLayout;
-		this.snippetThreshold = snippetThreshold;
-
-		try {
-			this.groupService = ThreemaApplication.getServiceManager().getGroupService();
-			this.contactService = ThreemaApplication.getServiceManager().getContactService();
-			this.ballotService = ThreemaApplication.getServiceManager().getBallotService();
-			this.hiddenChatsListService = ThreemaApplication.getServiceManager().getHiddenChatsListService();
-		} catch (Exception e) {
-			logger.error("Unable to get Services", e);
-		}
-
-		this.foregroundColor = ConfigUtils.getColorFromAttribute(context, R.attr.colorOnBackground);
-		this.colorStateListSend = ContextCompat.getColorStateList(context, R.color.bubble_send_colorstatelist);
-		this.colorStateListReceive = ContextCompat.getColorStateList(context, R.color.bubble_receive_colorstatelist);
-	}
-
-	@NonNull
-	@Override
-	public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
-		View v = LayoutInflater.from(parent.getContext())
-			.inflate(itemLayout, parent, false);
-
-		return new ItemHolder(v);
-	}
-
-	@Override
-	public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
-		ItemHolder itemHolder = (ItemHolder) holder;
-
-		if (messageModels != null) {
-			AbstractMessageModel current = getItem(position);
-
-			if (itemHolder.messageBlock != null) {
-				if (current.isOutbox()) {
-					itemHolder.messageBlock.setCardBackgroundColor(colorStateListSend);
-				} else {
-					itemHolder.messageBlock.setCardBackgroundColor(colorStateListReceive);
-				}
-			}
-
-			itemHolder.snippetView.setVisibility(View.VISIBLE);
-			if (itemHolder.deletedPlaceholder != null) {
-				itemHolder.deletedPlaceholder.setVisibility(View.GONE);
-			}
-
-			if (hiddenChatsListService.has(
-				current instanceof GroupMessageModel ?
-					groupService.getUniqueIdString(
-						((GroupMessageModel) current).getGroupId()
-					) :
-					contactService.getUniqueIdString(
-						current.getIdentity()
-					)
-			)) {
-				itemHolder.dateView.setText("");
-				itemHolder.thumbnailView.setVisibility(View.GONE);
-				itemHolder.titleView.setText("");
-				itemHolder.snippetView.setText(R.string.private_chat_subject);
-				itemHolder.avatarListItemHolder.avatarView.setVisibility(View.INVISIBLE);
-				itemHolder.avatarListItemHolder.avatarView.setBadgeVisible(false);
-			} else if (current.isDeleted()) {
-				// deleted placeholder for starred items - global search will never find deleted items
-				initDeletedViewHolder(itemHolder, current);
-			} else {
-				if (current instanceof GroupMessageModel) {
-					final GroupModel groupModel = groupService.getById(((GroupMessageModel) current).getGroupId());
-					AvatarListItemUtil.loadAvatar(
-						groupModel,
-						groupService,
-						itemHolder.avatarListItemHolder,
-						requestManager
-					);
-
-					itemHolder.titleView.setText(getTitle((GroupMessageModel) current, groupModel));
-				} else {
-					final ContactModel contactModel = this.contactService.getByIdentity(current.getIdentity());
-					AvatarListItemUtil.loadAvatar(
-						current.isOutbox() ? contactService.getMe() : contactModel,
-						contactService,
-						itemHolder.avatarListItemHolder,
-						requestManager
-					);
-
-					itemHolder.titleView.setText(getTitle(current, contactModel));
-				}
-				itemHolder.dateView.setText(LocaleUtil.formatDateRelative(current.getCreatedAt().getTime()));
-
-				if (current.getType() == MessageType.FILE && current.getFileData().isDownloaded()) {
-					loadThumbnail(current, itemHolder);
-					itemHolder.thumbnailView.setVisibility(View.VISIBLE);
-				} else if (current.getType() == MessageType.BALLOT) {
-					itemHolder.thumbnailView.setImageResource(R.drawable.ic_outline_rule);
-					itemHolder.thumbnailView.setVisibility(View.VISIBLE);
-					setupPlaceholder(itemHolder);
-				} else {
-					itemHolder.thumbnailView.setVisibility(View.GONE);
-				}
-
-				setSnippetToTextView(current, itemHolder);
-
-				if (itemHolder.snippetView.getText() == null || itemHolder.snippetView.getText().length() == 0) {
-					if (current.getType() == MessageType.FILE) {
-						String mimeString = current.getFileData().getMimeType();
-						if (!TestUtil.isEmptyOrNull(mimeString)) {
-							itemHolder.snippetView.setText(MimeUtil.getMimeDescription(context, current.getFileData().getMimeType()));
-						} else {
-							itemHolder.snippetView.setText("");
-						}
-					}
-				}
-			}
-
-			if (this.onClickItemListener != null) {
-				itemHolder.itemView.setOnClickListener(v -> onClickItemListener.onClick(current, itemHolder.itemView, position));
-				itemHolder.itemView.setOnLongClickListener(v -> onClickItemListener.onLongClick(current, itemHolder.itemView, position));
-			}
-
-			((CheckableRelativeLayout) holder.itemView).setChecked(checkedItems.get(position));
-		} else {
-			// Covers the case of data not being ready yet.
-			itemHolder.titleView.setText("No data");
-			itemHolder.dateView.setText("");
-			itemHolder.snippetView.setText("");
-		}
-	}
-
-	private void initDeletedViewHolder(ItemHolder holder, AbstractMessageModel message) {
-		holder.snippetView.setVisibility(View.INVISIBLE);
-		holder.snippetView.setText("");
-
-		if (holder.deletedPlaceholder != null) {
-			holder.deletedPlaceholder.setVisibility(View.VISIBLE);
-			holder.deletedPlaceholder.setText(R.string.message_was_deleted);
-		}
-
-		holder.dateView.setText(LocaleUtil.formatDateRelative(message.getCreatedAt().getTime()));
-
-		if (message instanceof GroupMessageModel) {
-			final GroupModel groupModel = groupService.getById(((GroupMessageModel) message).getGroupId());
-			holder.titleView.setText(getTitle((GroupMessageModel) message, groupModel));
-		} else {
-			final ContactModel contactModel = this.contactService.getByIdentity(message.getIdentity());
-			holder.titleView.setText(getTitle(message, contactModel));
-		}
-	}
-
-	private String getTitle(AbstractMessageModel messageModel, ContactModel contactModel) {
-		String name = NameUtil.getDisplayNameOrNickname(context, messageModel, contactService);
-		return messageModel.isOutbox() ?
-			name + " " + FLOW_CHARACTER + " " + NameUtil.getDisplayNameOrNickname(contactModel, true) :
-			name;
-	}
-
-	private String getTitle(GroupMessageModel messageModel, GroupModel groupModel) {
-		final ContactModel contactModel = messageModel.isOutbox() ? this.contactService.getMe() : this.contactService.getByIdentity(messageModel.getIdentity());
-		String groupName = NameUtil.getDisplayName(groupModel, groupService);
-		return String.format("%s %s %s", NameUtil.getDisplayNameOrNickname(contactModel, true), FLOW_CHARACTER, groupName);
-	}
-
-	private void loadThumbnail(AbstractMessageModel messageModel, ItemHolder holder) {
-		@DrawableRes int placeholderIcon;
-
-		if (messageModel.getMessageContentsType() == MessageContentsType.VOICE_MESSAGE) {
-			placeholderIcon = R.drawable.ic_keyboard_voice_outline;
-		} else if (messageModel.getType() == MessageType.FILE) {
-			placeholderIcon = IconUtil.getMimeIcon(messageModel.getFileData().getMimeType());
-		} else {
-			placeholderIcon = IconUtil.getMimeIcon("application/x-error");
-		}
-
-		Glide.with(context)
-			.asBitmap()
-			.load(messageModel)
-			.transition(BitmapTransitionOptions.withCrossFade())
-			.centerCrop()
-			.error(placeholderIcon)
-			.addListener(new RequestListener<>() {
-				@Override
-				public boolean onLoadFailed(@Nullable GlideException e, Object model, @NonNull Target<Bitmap> target, boolean isFirstResource) {
-					setupPlaceholder(holder);
-					return false;
-				}
-
-				@Override
-				public boolean onResourceReady(@NonNull Bitmap resource, @NonNull Object model, Target<Bitmap> target, @NonNull DataSource dataSource, boolean isFirstResource) {
-					holder.thumbnailView.clearColorFilter();
-					holder.thumbnailView.setScaleType(ImageView.ScaleType.CENTER_CROP);
-					return false;
-				}
-			})
-            .into(holder.thumbnailView);
-	}
-
-	private void setupPlaceholder(ItemHolder holder) {
-		holder.thumbnailView.setColorFilter(foregroundColor, PorterDuff.Mode.SRC_IN);
-		holder.thumbnailView.setScaleType(ImageView.ScaleType.CENTER);
-	}
-
-	/**
-	 * Returns a snippet containing the first occurrence of needle in fullText
-	 * Splits text on emoji boundary
-	 * Note: the match is case-insensitive
-	 *
-	 * @param fullText Full text
-	 * @param needle   Text to search for
-	 * @param holder   ItemHolder containing a textview
-	 * @return Snippet containing the match with a trailing ellipsis if the match is located beyond the first snippetThreshold characters
-	 */
-	private CharSequence getSnippet(@NonNull String fullText, @Nullable String needle, ItemHolder holder) {
-		if (!TestUtil.isEmptyOrNull(needle)) {
-			int firstMatch = fullText.toLowerCase().indexOf(needle);
-			if (firstMatch > snippetThreshold) {
-				int snippetStart = firstMatch > (snippetThreshold + 3) ? firstMatch - (snippetThreshold + 3) : 0;
-
-				for (int i = snippetStart; i < firstMatch; i++) {
-					if (Character.isWhitespace(fullText.charAt(i))) {
-						return "…" + fullText.substring(i + 1);
-					}
-				}
-
-				SpannableStringBuilder emojified = (SpannableStringBuilder) EmojiMarkupUtil.getInstance().addTextSpans(context, fullText, holder.snippetView, true, false, false);
-
-				int transitionStart = emojified.nextSpanTransition(firstMatch - snippetThreshold, firstMatch, EmojiImageSpan.class);
-				if (transitionStart == firstMatch) {
-					// there are no spans here
-					return "…" + emojified.subSequence(firstMatch - snippetThreshold, emojified.length());
-				} else {
-					return "…" + emojified.subSequence(transitionStart, emojified.length());
-				}
-			}
-		}
-		return EmojiMarkupUtil.getInstance().addTextSpans(context, fullText, holder.snippetView, false, false, false);
-	}
-
-
-	public void setMessageModels(List<AbstractMessageModel> messageModels) {
-		this.messageModels = messageModels;
-		notifyDataSetChanged();
-	}
-
-	private void setSnippetToTextView(AbstractMessageModel current, ItemHolder itemHolder) {
-		CharSequence snippetText = null;
-		switch (current.getType()) {
-			case FILE:
-				// fallthrough
-			case IMAGE:
-				if (!TestUtil.isEmptyOrNull(current.getCaption())) {
-					snippetText = getSnippet(current.getCaption(), this.queryString, itemHolder);
-				}
-				break;
-			case TEXT:
-				if (!TestUtil.isEmptyOrNull(current.getBody())) {
-					snippetText = getSnippet(current.getBody(), this.queryString, itemHolder);
-				}
-				break;
-			case BALLOT:
-				snippetText = context.getString(R.string.attach_ballot);
-				if (!TestUtil.isEmptyOrNull(current.getBody())) {
-					BallotDataModel ballotData = current.getBallotData();
-					final BallotModel ballotModel = ballotService.get(ballotData.getBallotId());
-					if (ballotModel != null) {
-						snippetText = getSnippet(ballotModel.getName(), this.queryString, itemHolder);
-					}
-				}
-				break;
-			case LOCATION:
-				final LocationDataModel location = current.getLocationData();
-				StringBuilder locationStringBuilder = new StringBuilder();
-				if (!TestUtil.isEmptyOrNull(location.getPoi())) {
-					locationStringBuilder.append(location.getPoi());
-				}
-				if (!TestUtil.isEmptyOrNull(location.getAddress())) {
-					if (locationStringBuilder.length() > 0) {
-						locationStringBuilder.append(" - ");
-					}
-					locationStringBuilder.append(location.getAddress());
-				}
-				if (locationStringBuilder.length() > 0) {
-					snippetText = getSnippet(locationStringBuilder.toString(), this.queryString, itemHolder);
-				}
-				break;
-			default:
-				// Audio and Video Messages don't have text or captions
-				break;
-		}
-
-		if (snippetText != null) {
-			itemHolder.snippetView.setText(TextUtil.highlightMatches(context, snippetText, this.queryString, true, false));
-		} else {
-			itemHolder.snippetView.setText(null);
-		}
-	}
-
-	private AbstractMessageModel getItem(int position) {
-		return messageModels.get(position);
-	}
-
-	// getItemCount() is called many times, and when it is first called,
-	// messageModels has not been updated (means initially, it's null, and we can't return null).
-	@Override
-	public int getItemCount() {
-		if (messageModels != null) {
-			return messageModels.size();
-		} else {
-			return 0;
-		}
-	}
-
-	public void toggleChecked(int pos) {
-		if (checkedItems.get(pos, false)) {
-			checkedItems.delete(pos);
-		}
-		else {
-			checkedItems.put(pos, true);
-		}
-		notifyItemChanged(pos);
-	}
-
-	public int getCheckedItemsCount() {
-		return checkedItems.size();
-	}
-
-	public List<AbstractMessageModel> getCheckedItems() {
-		List<AbstractMessageModel> items = new ArrayList<>(checkedItems.size());
-		for (int i = 0; i < checkedItems.size(); i++) {
-			items.add(messageModels.get(checkedItems.keyAt(i)));
-		}
-		return items;
-	}
-
-	public void clearCheckedItems() {
-		checkedItems.clear();
-		notifyDataSetChanged();
-	}
-
-	public void setOnClickItemListener(OnClickItemListener onClickItemListener) {
-		this.onClickItemListener = onClickItemListener;
-	}
-
-	public void onQueryChanged(String queryText) {
-		this.queryString = queryText;
-	}
-
-	public interface OnClickItemListener {
-		void onClick(AbstractMessageModel messageModel, View view, int position);
-		default boolean onLongClick(AbstractMessageModel messageModel, View view, int position) {
-			return false;
-		}
-	}
+    private static final Logger logger = LoggingUtil.getThreemaLogger("GlobalSearchAdapter");
+    private static final String FLOW_CHARACTER = "\u25BA\uFE0E";
+
+    private GroupService groupService;
+    private ContactService contactService;
+    private BallotService ballotService;
+    private DeadlineListService hiddenChatsListService;
+
+    private final Context context;
+    private OnClickItemListener onClickItemListener;
+    private final SparseBooleanArray checkedItems = new SparseBooleanArray();
+    private String queryString;
+    private final int snippetThreshold;
+    private List<AbstractMessageModel> messageModels; // Cached copy of AbstractMessageModels
+    private final @LayoutRes int itemLayout;
+    private final ColorStateList colorStateListSend, colorStateListReceive;
+    private final @NonNull RequestManager requestManager;
+    private final boolean appUsesDynamicColors;
+
+    private static class ViewHolder extends RecyclerView.ViewHolder {
+        private final TextView titleView;
+        private final TextView dateView;
+        private final TextView snippetView;
+        @Nullable
+        private final TextView deletedPlaceholder; // will be null for layouts other than item_starred_messages
+        private final ImageView thumbnailView;
+        private final MaterialCardView messageBlock;
+        AvatarListItemHolder avatarListItemHolder;
+
+        private ViewHolder(final View itemView) {
+            super(itemView);
+
+            titleView = itemView.findViewById(R.id.name);
+            dateView = itemView.findViewById(R.id.date);
+            snippetView = itemView.findViewById(R.id.snippet);
+            deletedPlaceholder = itemView.findViewById(R.id.deleted_placeholder);
+            AvatarView avatarView = itemView.findViewById(R.id.avatar_view);
+            thumbnailView = itemView.findViewById(R.id.thumbnail_view);
+            messageBlock = itemView.findViewById(R.id.message_block);
+
+            avatarListItemHolder = new AvatarListItemHolder();
+            avatarListItemHolder.avatarView = avatarView;
+            avatarListItemHolder.avatarLoadingAsyncTask = null;
+        }
+
+        protected boolean getHasBubbleBackground() {
+            return this.messageBlock != null;
+        }
+    }
+
+    public GlobalSearchAdapter(
+        @NonNull Context context,
+        @NonNull RequestManager requestManager,
+        @LayoutRes int itemLayout,
+        int snippetThreshold
+    ) {
+        this.context = context;
+        this.requestManager = requestManager;
+        this.itemLayout = itemLayout;
+        this.snippetThreshold = snippetThreshold;
+
+        try {
+            this.groupService = ThreemaApplication.requireServiceManager().getGroupService();
+            this.contactService = ThreemaApplication.requireServiceManager().getContactService();
+            this.ballotService = ThreemaApplication.requireServiceManager().getBallotService();
+            this.hiddenChatsListService = ThreemaApplication.requireServiceManager().getHiddenChatsListService();
+        } catch (Exception e) {
+            logger.error("Unable to get Services", e);
+        }
+
+        this.colorStateListSend = ContextCompat.getColorStateList(context, R.color.bubble_send_colorstatelist);
+        this.colorStateListReceive = ContextCompat.getColorStateList(context, R.color.bubble_receive_colorstatelist);
+
+        this.appUsesDynamicColors = ColorUtil.areDynamicColorsCurrentlyApplied(context);
+    }
+
+    @NonNull
+    @Override
+    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+        View itemView = LayoutInflater.from(parent.getContext())
+            .inflate(itemLayout, parent, false);
+        return new ViewHolder(itemView);
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+        ViewHolder viewHolder = (ViewHolder) holder;
+
+        if (messageModels != null) {
+            AbstractMessageModel messageModel = getItem(position);
+
+            if (viewHolder.getHasBubbleBackground()) {
+                if (messageModel.isOutbox()) {
+                    viewHolder.messageBlock.setCardBackgroundColor(colorStateListSend);
+                } else {
+                    viewHolder.messageBlock.setCardBackgroundColor(colorStateListReceive);
+                }
+
+                viewHolder.titleView.setTextColor(messageModel.getUiContentColor(context));
+                viewHolder.snippetView.setTextColor(messageModel.getUiContentColor(context));
+                viewHolder.dateView.setTextColor(messageModel.getUiContentColor(context));
+            }
+
+            viewHolder.snippetView.setVisibility(View.VISIBLE);
+            if (viewHolder.deletedPlaceholder != null) {
+                viewHolder.deletedPlaceholder.setVisibility(View.GONE);
+            }
+
+            final @NonNull String uid = messageModel instanceof GroupMessageModel
+                ? groupService.getUniqueIdString(((GroupMessageModel) messageModel).getGroupId())
+                : contactService.getUniqueIdString(messageModel.getIdentity());
+            if (hiddenChatsListService.has(uid)) {
+                viewHolder.dateView.setText("");
+                viewHolder.thumbnailView.setVisibility(View.GONE);
+                viewHolder.titleView.setText("");
+                viewHolder.snippetView.setText(R.string.private_chat_subject);
+                viewHolder.avatarListItemHolder.avatarView.setVisibility(View.INVISIBLE);
+                viewHolder.avatarListItemHolder.avatarView.setBadgeVisible(false);
+            } else if (messageModel.isDeleted()) {
+                // deleted placeholder for starred items - global search will never find deleted items
+                initDeletedViewHolder(viewHolder, messageModel);
+            } else {
+                if (messageModel instanceof GroupMessageModel) {
+                    final GroupModel groupModel = groupService.getById(((GroupMessageModel) messageModel).getGroupId());
+                    AvatarListItemUtil.loadAvatar(
+                        groupModel,
+                        groupService,
+                        viewHolder.avatarListItemHolder,
+                        requestManager
+                    );
+
+                    viewHolder.titleView.setText(getTitle((GroupMessageModel) messageModel, groupModel));
+                } else {
+                    final ContactModel contactModel = this.contactService.getByIdentity(messageModel.getIdentity());
+                    AvatarListItemUtil.loadAvatar(
+                        messageModel.isOutbox() ? contactService.getMe() : contactModel,
+                        contactService,
+                        viewHolder.avatarListItemHolder,
+                        requestManager
+                    );
+
+                    viewHolder.titleView.setText(getTitle(messageModel, contactModel));
+                }
+                viewHolder.dateView.setText(LocaleUtil.formatDateRelative(messageModel.getCreatedAt().getTime()));
+
+                if (messageModel.getType() == MessageType.FILE && messageModel.getFileData().isDownloaded()) {
+                    loadThumbnail(messageModel, viewHolder);
+                    viewHolder.thumbnailView.setVisibility(View.VISIBLE);
+                } else if (messageModel.getType() == MessageType.BALLOT) {
+                    viewHolder.thumbnailView.setImageResource(R.drawable.ic_outline_rule);
+                    viewHolder.thumbnailView.setVisibility(View.VISIBLE);
+                    setupPlaceholder(viewHolder, messageModel);
+                } else {
+                    viewHolder.thumbnailView.setVisibility(View.GONE);
+                }
+
+                setSnippetToTextView(messageModel, viewHolder);
+
+                if (viewHolder.snippetView.getText() == null || viewHolder.snippetView.getText().length() == 0) {
+                    if (messageModel.getType() == MessageType.FILE) {
+                        String mimeString = messageModel.getFileData().getMimeType();
+                        if (!TestUtil.isEmptyOrNull(mimeString)) {
+                            viewHolder.snippetView.setText(MimeUtil.getMimeDescription(context, messageModel.getFileData().getMimeType()));
+                        } else {
+                            viewHolder.snippetView.setText("");
+                        }
+                    }
+                }
+            }
+
+            if (this.onClickItemListener != null) {
+                viewHolder.itemView.setOnClickListener(v -> onClickItemListener.onClick(messageModel, viewHolder.itemView, position));
+                viewHolder.itemView.setOnLongClickListener(v -> onClickItemListener.onLongClick(messageModel, viewHolder.itemView, position));
+            }
+
+            ((CheckableRelativeLayout) viewHolder.itemView).setChecked(checkedItems.get(position));
+        } else {
+            // Covers the case of data not being ready yet.
+            viewHolder.titleView.setText("No data");
+            viewHolder.dateView.setText("");
+            viewHolder.snippetView.setText("");
+        }
+    }
+
+    private void initDeletedViewHolder(@NonNull ViewHolder viewHolder, AbstractMessageModel message) {
+        viewHolder.snippetView.setVisibility(View.INVISIBLE);
+        viewHolder.snippetView.setText("");
+
+        if (viewHolder.deletedPlaceholder != null) {
+            viewHolder.deletedPlaceholder.setVisibility(View.VISIBLE);
+            viewHolder.deletedPlaceholder.setText(R.string.message_was_deleted);
+            if (viewHolder.getHasBubbleBackground()) {
+                assert viewHolder.deletedPlaceholder != null;
+                viewHolder.deletedPlaceholder.setTextColor(message.getUiContentColor(viewHolder.deletedPlaceholder.getContext()));
+            }
+        }
+
+        viewHolder.dateView.setText(LocaleUtil.formatDateRelative(message.getCreatedAt().getTime()));
+
+        if (message instanceof GroupMessageModel) {
+            final GroupModel groupModel = groupService.getById(((GroupMessageModel) message).getGroupId());
+            viewHolder.titleView.setText(getTitle((GroupMessageModel) message, groupModel));
+        } else {
+            final ContactModel contactModel = this.contactService.getByIdentity(message.getIdentity());
+            viewHolder.titleView.setText(getTitle(message, contactModel));
+        }
+    }
+
+    private String getTitle(AbstractMessageModel messageModel, ContactModel contactModel) {
+        String name = NameUtil.getDisplayNameOrNickname(context, messageModel, contactService);
+        return messageModel.isOutbox() ?
+            name + " " + FLOW_CHARACTER + " " + NameUtil.getDisplayNameOrNickname(contactModel, true) :
+            name;
+    }
+
+    @NonNull
+    private String getTitle(@NonNull GroupMessageModel messageModel, GroupModel groupModel) {
+        final ContactModel contactModel = messageModel.isOutbox() ? this.contactService.getMe() : this.contactService.getByIdentity(messageModel.getIdentity());
+        String groupName = NameUtil.getDisplayName(groupModel, groupService);
+        return String.format("%s %s %s", NameUtil.getDisplayNameOrNickname(contactModel, true), FLOW_CHARACTER, groupName);
+    }
+
+    private void loadThumbnail(@NonNull AbstractMessageModel messageModel, ViewHolder viewHolder) {
+        @DrawableRes int placeholderIcon;
+
+        if (messageModel.getMessageContentsType() == MessageContentsType.VOICE_MESSAGE) {
+            placeholderIcon = R.drawable.ic_keyboard_voice_outline;
+        } else if (messageModel.getType() == MessageType.FILE) {
+            placeholderIcon = IconUtil.getMimeIcon(messageModel.getFileData().getMimeType());
+        } else {
+            placeholderIcon = IconUtil.getMimeIcon("application/x-error");
+        }
+
+        Glide.with(context)
+            .asBitmap()
+            .load(messageModel)
+            .transition(BitmapTransitionOptions.withCrossFade())
+            .centerCrop()
+            .error(placeholderIcon)
+            .addListener(new RequestListener<>() {
+                @Override
+                public boolean onLoadFailed(@Nullable GlideException e, Object model, @NonNull Target<Bitmap> target, boolean isFirstResource) {
+                    setupPlaceholder(viewHolder, messageModel);
+                    return false;
+                }
+
+                @Override
+                public boolean onResourceReady(@NonNull Bitmap resource, @NonNull Object model, Target<Bitmap> target, @NonNull DataSource dataSource, boolean isFirstResource) {
+                    viewHolder.thumbnailView.clearColorFilter();
+                    viewHolder.thumbnailView.setScaleType(ImageView.ScaleType.CENTER_CROP);
+                    return false;
+                }
+            })
+            .into(viewHolder.thumbnailView);
+    }
+
+    private void setupPlaceholder(@NonNull ViewHolder viewHolder, @NonNull AbstractMessageModel messageModel) {
+        viewHolder.thumbnailView.setBackgroundColor(getThumbnailViewBackgroundColor(messageModel));
+        viewHolder.thumbnailView.setColorFilter(getThumbnailViewIconTintColor(messageModel), PorterDuff.Mode.SRC_IN);
+        viewHolder.thumbnailView.setScaleType(ImageView.ScaleType.CENTER);
+    }
+
+    @ColorInt
+    private int getThumbnailViewBackgroundColor(@NonNull AbstractMessageModel messageModel) {
+        if (shouldApplyDynamicColorsToMessageItemView(messageModel)) {
+            return ConfigUtils.getColorFromAttribute(context, R.attr.colorPrimary);
+        } else {
+            return context.getResources().getColor(
+                ColorUtil.shouldUseDarkVariant(context)
+                    ? R.color.md_theme_dark_tertiaryContainer
+                    : R.color.md_theme_light_tertiaryContainer
+            );
+        }
+    }
+
+    @ColorInt
+    private int getThumbnailViewIconTintColor(@NonNull AbstractMessageModel messageModel) {
+        if (shouldApplyDynamicColorsToMessageItemView(messageModel)) {
+            return ConfigUtils.getColorFromAttribute(context, R.attr.colorOnPrimary);
+        } else {
+            return context.getResources().getColor(
+                ColorUtil.shouldUseDarkVariant(context)
+                    ? R.color.md_theme_dark_onTertiaryContainer
+                    : R.color.md_theme_light_onTertiaryContainer
+            );
+        }
+    }
+
+    /**
+     * Returns a snippet containing the first occurrence of needle in fullText
+     * Splits text on emoji boundary
+     * Note: the match is case-insensitive
+     *
+     * @param fullText   Full text
+     * @param needle     Text to search for
+     * @param viewHolder ItemHolder containing a textview
+     * @return Snippet containing the match with a trailing ellipsis if the match is located beyond the first snippetThreshold characters
+     */
+    private CharSequence getSnippet(@NonNull String fullText, @Nullable String needle, ViewHolder viewHolder) {
+        if (!TestUtil.isEmptyOrNull(needle)) {
+            int firstMatch = fullText.toLowerCase().indexOf(needle);
+            if (firstMatch > snippetThreshold) {
+                int snippetStart = firstMatch > (snippetThreshold + 3) ? firstMatch - (snippetThreshold + 3) : 0;
+
+                for (int i = snippetStart; i < firstMatch; i++) {
+                    if (Character.isWhitespace(fullText.charAt(i))) {
+                        return "…" + fullText.substring(i + 1);
+                    }
+                }
+
+                SpannableStringBuilder emojified = (SpannableStringBuilder) EmojiMarkupUtil.getInstance().addTextSpans(context, fullText, viewHolder.snippetView, true, false, false);
+
+                int transitionStart = emojified.nextSpanTransition(firstMatch - snippetThreshold, firstMatch, EmojiImageSpan.class);
+                if (transitionStart == firstMatch) {
+                    // there are no spans here
+                    return "…" + emojified.subSequence(firstMatch - snippetThreshold, emojified.length());
+                } else {
+                    return "…" + emojified.subSequence(transitionStart, emojified.length());
+                }
+            }
+        }
+        return EmojiMarkupUtil.getInstance().addTextSpans(context, fullText, viewHolder.snippetView, false, false, false);
+    }
+
+
+    public void setMessageModels(List<AbstractMessageModel> messageModels) {
+        this.messageModels = messageModels;
+        notifyDataSetChanged();
+    }
+
+    private void setSnippetToTextView(@NonNull AbstractMessageModel messageModel, ViewHolder viewHolder) {
+        CharSequence snippetText = null;
+        switch (messageModel.getType()) {
+            case FILE:
+                // fallthrough
+            case IMAGE:
+                if (!TestUtil.isEmptyOrNull(messageModel.getCaption())) {
+                    snippetText = getSnippet(messageModel.getCaption(), this.queryString, viewHolder);
+                }
+                break;
+            case TEXT:
+                if (!TestUtil.isEmptyOrNull(messageModel.getBody())) {
+                    snippetText = getSnippet(messageModel.getBody(), this.queryString, viewHolder);
+                }
+                break;
+            case BALLOT:
+                snippetText = context.getString(R.string.attach_ballot);
+                if (!TestUtil.isEmptyOrNull(messageModel.getBody())) {
+                    BallotDataModel ballotData = messageModel.getBallotData();
+                    final BallotModel ballotModel = ballotService.get(ballotData.getBallotId());
+                    if (ballotModel != null) {
+                        snippetText = getSnippet(ballotModel.getName(), this.queryString, viewHolder);
+                    }
+                }
+                break;
+            case LOCATION:
+                final LocationDataModel location = messageModel.getLocationData();
+                StringBuilder locationStringBuilder = new StringBuilder();
+                if (!TestUtil.isEmptyOrNull(location.getPoi())) {
+                    locationStringBuilder.append(location.getPoi());
+                }
+                if (!TestUtil.isEmptyOrNull(location.getAddress())) {
+                    if (locationStringBuilder.length() > 0) {
+                        locationStringBuilder.append(" - ");
+                    }
+                    locationStringBuilder.append(location.getAddress());
+                }
+                if (locationStringBuilder.length() > 0) {
+                    snippetText = getSnippet(locationStringBuilder.toString(), this.queryString, viewHolder);
+                }
+                break;
+            default:
+                // Audio and Video Messages don't have text or captions
+                break;
+        }
+
+        if (snippetText != null) {
+            viewHolder.snippetView.setText(TextUtil.highlightMatches(context, snippetText, this.queryString, true, false));
+        } else {
+            viewHolder.snippetView.setText(null);
+        }
+    }
+
+    private AbstractMessageModel getItem(int position) {
+        return messageModels.get(position);
+    }
+
+    // getItemCount() is called many times, and when it is first called,
+    // messageModels has not been updated (means initially, it's null, and we can't return null).
+    @Override
+    public int getItemCount() {
+        if (messageModels != null) {
+            return messageModels.size();
+        } else {
+            return 0;
+        }
+    }
+
+    public void toggleChecked(int pos) {
+        if (checkedItems.get(pos, false)) {
+            checkedItems.delete(pos);
+        } else {
+            checkedItems.put(pos, true);
+        }
+        notifyItemChanged(pos);
+    }
+
+    public int getCheckedItemsCount() {
+        return checkedItems.size();
+    }
+
+    public List<AbstractMessageModel> getCheckedItems() {
+        List<AbstractMessageModel> items = new ArrayList<>(checkedItems.size());
+        for (int i = 0; i < checkedItems.size(); i++) {
+            items.add(messageModels.get(checkedItems.keyAt(i)));
+        }
+        return items;
+    }
+
+    public void clearCheckedItems() {
+        checkedItems.clear();
+        notifyDataSetChanged();
+    }
+
+    public void setOnClickItemListener(OnClickItemListener onClickItemListener) {
+        this.onClickItemListener = onClickItemListener;
+    }
+
+    public void onQueryChanged(String queryText) {
+        this.queryString = queryText;
+    }
+
+    /**
+     * We only want to apply dynamic colors if the current item view is used to
+     * render an outbox message <strong>and</strong> they are enabled by the
+     * threema setting. This workaround is necessary because we actually use
+     * different color references when this returns true.
+     */
+    private boolean shouldApplyDynamicColorsToMessageItemView(@NonNull AbstractMessageModel messageModel) {
+        if (!appUsesDynamicColors) {
+            return false;
+        }
+        return messageModel.isOutbox();
+    }
+
+    public interface OnClickItemListener {
+        void onClick(AbstractMessageModel messageModel, View view, int position);
+
+        default boolean onLongClick(AbstractMessageModel messageModel, View view, int position) {
+            return false;
+        }
+    }
 }

+ 13 - 13
app/src/main/java/ch/threema/app/managers/ServiceManager.java

@@ -229,7 +229,7 @@ public class ServiceManager {
 	private SystemScreenLockService systemScreenLockService;
 
 	@Nullable
-	private IdListService blackListService, excludedSyncIdentitiesService, profilePicRecipientsService;
+	private IdListService blockedContactsService, excludedSyncIdentitiesService, profilePicRecipientsService;
 	@Nullable
 	private DeadlineListService mutedChatsListService, hiddenChatListService, mentionOnlyChatsListService;
 	@Nullable
@@ -451,7 +451,7 @@ public class ServiceManager {
 				this.getUserService(),
 				this.getIdentityStore(),
 				this.getPreferenceService(),
-				this.getBlackListService(),
+				this.getBlockedContactsService(),
 				this.getProfilePicRecipientsService(),
 				this.getRingtoneService(),
 				this.getMutedChatsListService(),
@@ -487,7 +487,7 @@ public class ServiceManager {
 					this.getApiService(),
 					this.getDownloadService(),
 					this.getHiddenChatsListService(),
-					this.getBlackListService(),
+					this.getBlockedContactsService(),
                     this.getModelRepositories().getEditHistory()
 			);
 		}
@@ -772,7 +772,7 @@ public class ServiceManager {
 				this.getDistributionListService(),
 				this.getMessageService(),
 				this.getHiddenChatsListService(),
-				this.getBlackListService(),
+				this.getBlockedContactsService(),
 				this.getConversationTagService()
 			);
 		}
@@ -818,8 +818,7 @@ public class ServiceManager {
 				this.getDeviceService(),
 				this.getFileService(),
 				this.getIdentityStore(),
-				this.getBlackListService(),
-				this.getLicenseService(),
+				this.getBlockedContactsService(),
 				this.getApiService()
 			);
 		}
@@ -828,11 +827,12 @@ public class ServiceManager {
 	}
 
 	@NonNull
-	public IdListService getBlackListService() {
-		if(this.blackListService == null) {
-			this.blackListService = new IdListServiceImpl("identity_list_blacklist", this.getPreferenceService());
+	public IdListService getBlockedContactsService() {
+		if(this.blockedContactsService == null) {
+            // Keep the uniqueListName `identity_list_blacklist` to avoid a migration of the key in the preferences
+			this.blockedContactsService = new IdListServiceImpl("identity_list_blacklist", this.getPreferenceService());
 		}
-		return this.blackListService;
+		return this.blockedContactsService;
 	}
 
 	@NonNull
@@ -937,7 +937,7 @@ public class ServiceManager {
 				this.getDistributionListService(),
 				this.getLocaleService(),
 				this.getFileService(),
-				this.getBlackListService(),
+				this.getBlockedContactsService(),
 				this.getExcludedSyncIdentitiesService(),
 				this.getProfilePicRecipientsService(),
 				this.getDatabaseServiceNew(),
@@ -1019,7 +1019,7 @@ public class ServiceManager {
 				this.getMessageService(),
 				this.getNotificationService(),
 				this.databaseServiceNew,
-				this.getBlackListService(),
+				this.getBlockedContactsService(),
 				this.getPreferenceService(),
 				this.getUserService(),
 				this.getHiddenChatsListService(),
@@ -1169,7 +1169,7 @@ public class ServiceManager {
 				getContactService(),
 				getContactStore(),
 				getIdentityStore(),
-				getBlackListService(),
+				getBlockedContactsService(),
 				getPreferenceService(),
 				this
 			);

+ 4 - 4
app/src/main/java/ch/threema/app/mediaattacher/MediaSelectionBaseActivity.java

@@ -374,7 +374,7 @@ abstract public class MediaSelectionBaseActivity extends ThreemaActivity impleme
 		this.mediaAttachRecyclerView.addItemDecoration(new MediaGridItemDecoration(getResources().getDimensionPixelSize(R.dimen.grid_spacing)));
 		this.mediaAttachRecyclerView.setAdapter(mediaAttachAdapter);
 
-		ConfigUtils.addIconsToOverflowMenu(this, this.toolbar.getMenu());
+		ConfigUtils.addIconsToOverflowMenu(this.toolbar.getMenu());
 
 		this.rootView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
 			@Override
@@ -413,7 +413,7 @@ abstract public class MediaSelectionBaseActivity extends ThreemaActivity impleme
 			return true;
 		});
 		topMenuItem.setIcon(R.drawable.ic_collections);
-		ConfigUtils.tintMenuItem(this, topMenuItem, R.attr.colorOnSurface);
+		ConfigUtils.tintMenuIcon(this, topMenuItem, R.attr.colorOnSurface);
 
 		// Fetch all media, add a unique menu item for each media storage bucket and media type group.
 		registerOnAllDataFetchedListener(new Observer<>() {
@@ -474,7 +474,7 @@ abstract public class MediaSelectionBaseActivity extends ThreemaActivity impleme
 								item.setIcon(R.drawable.ic_webp);
 								break;
 						}
-						ConfigUtils.tintMenuItem(MediaSelectionBaseActivity.this, item, R.attr.colorOnSurface);
+						ConfigUtils.tintMenuIcon(MediaSelectionBaseActivity.this, item, R.attr.colorOnSurface);
 					}
 
 					for (String bucket : buckets) {
@@ -484,7 +484,7 @@ abstract public class MediaSelectionBaseActivity extends ThreemaActivity impleme
 								return true;
 							});
 							item.setIcon(R.drawable.ic_outline_folder_24);
-							ConfigUtils.tintMenuItem(MediaSelectionBaseActivity.this, item, R.attr.colorOnSurface);
+							ConfigUtils.tintMenuIcon(MediaSelectionBaseActivity.this, item, R.attr.colorOnSurface);
 						}
 					}
 

+ 5 - 5
app/src/main/java/ch/threema/app/messagereceiver/ContactMessageReceiver.java

@@ -88,7 +88,7 @@ public class ContactMessageReceiver implements MessageReceiver<MessageModel> {
 	private final ServiceManager serviceManager;
 	private final DatabaseServiceNew databaseServiceNew;
 	private final IdentityStore identityStore;
-	private final IdListService blackListIdentityService;
+	private final IdListService blockedContactsService;
 	private final @NonNull TaskManager taskManager;
 
 	public ContactMessageReceiver(ContactModel contactModel,
@@ -96,13 +96,13 @@ public class ContactMessageReceiver implements MessageReceiver<MessageModel> {
 	                              @NonNull ServiceManager serviceManager,
 	                              DatabaseServiceNew databaseServiceNew,
 	                              IdentityStore identityStore,
-	                              IdListService blackListIdentityService) {
+	                              IdListService blockedContactsService) {
 		this.contactModel = contactModel;
 		this.contactService = contactService;
 		this.serviceManager = serviceManager;
 		this.databaseServiceNew = databaseServiceNew;
 		this.identityStore = identityStore;
-		this.blackListIdentityService = blackListIdentityService;
+		this.blockedContactsService = blockedContactsService;
 		this.taskManager = serviceManager.getTaskManager();
 	}
 
@@ -113,7 +113,7 @@ public class ContactMessageReceiver implements MessageReceiver<MessageModel> {
 			contactMessageReceiver.serviceManager,
 			contactMessageReceiver.databaseServiceNew,
 			contactMessageReceiver.identityStore,
-			contactMessageReceiver.blackListIdentityService
+			contactMessageReceiver.blockedContactsService
 		);
 		avatar = contactMessageReceiver.avatar;
 	}
@@ -552,7 +552,7 @@ public class ContactMessageReceiver implements MessageReceiver<MessageModel> {
 	@Override
 	public SendingPermissionValidationResult validateSendingPermission() {
 		int cannotSendResId = 0;
-		if (blackListIdentityService.has(contactModel.getIdentity())) {
+		if (blockedContactsService.has(contactModel.getIdentity())) {
 			cannotSendResId = R.string.blocked_cannot_send;
 		} else {
 			if (contactModel.getState() != null) {

+ 1 - 1
app/src/main/java/ch/threema/app/multidevice/linking/DeviceJoinDataCollector.kt

@@ -120,7 +120,7 @@ class DeviceJoinDataCollector(
     private val distributionListService by lazy { serviceManager.distributionListService }
     private val deviceCookieManager by lazy { serviceManager.deviceCookieManager }
     private val preferenceService by lazy { serviceManager.preferenceService }
-    private val blockedIdentitiesService by lazy { serviceManager.blackListService }
+    private val blockedIdentitiesService by lazy { serviceManager.blockedContactsService }
     private val excludeFromSyncService by lazy { serviceManager.excludedSyncIdentitiesService }
     private val fileService by lazy { serviceManager.fileService }
     private val hiddenChatsService by lazy { serviceManager.hiddenChatsListService }

+ 5 - 5
app/src/main/java/ch/threema/app/preference/SettingsPrivacyFragment.kt

@@ -37,7 +37,7 @@ import androidx.preference.TwoStatePreference
 import androidx.work.WorkManager
 import ch.threema.app.R
 import ch.threema.app.ThreemaApplication
-import ch.threema.app.activities.BlackListActivity
+import ch.threema.app.activities.BlockedContactsActivity
 import ch.threema.app.activities.ExcludedSyncIdentitiesActivity
 import ch.threema.app.dialogs.GenericAlertDialog
 import ch.threema.app.dialogs.GenericProgressDialog
@@ -110,7 +110,7 @@ class SettingsPrivacyFragment : ThreemaPreferenceFragment(), GenericAlertDialog.
 
         initExcludedSyncIdentitiesPref()
 
-        initBlackListPref()
+        initBlockedContactsPref()
 
         initResetReceiptsPref()
 
@@ -216,9 +216,9 @@ class SettingsPrivacyFragment : ThreemaPreferenceFragment(), GenericAlertDialog.
         }
     }
 
-    private fun initBlackListPref() {
-        getPref<Preference>("pref_black_list").setOnPreferenceClickListener {
-            startActivity(Intent(activity, BlackListActivity::class.java))
+    private fun initBlockedContactsPref() {
+        getPref<Preference>("pref_blocked_contacts").setOnPreferenceClickListener {
+            startActivity(Intent(activity, BlockedContactsActivity::class.java))
             false
         }
     }

+ 2 - 2
app/src/main/java/ch/threema/app/processors/IncomingMessageProcessorImpl.kt

@@ -120,7 +120,7 @@ class IncomingMessageProcessorImpl(
     private val contactService: ContactService,
     private val contactStore: ContactStore,
     private val identityStore: IdentityStoreInterface,
-    private val blackListService: IdListService,
+    private val blockedContactsService: IdListService,
     private val preferenceService: PreferenceService,
     private val serviceManager: ServiceManager,
 ) : IncomingMessageProcessor {
@@ -459,7 +459,7 @@ class IncomingMessageProcessorImpl(
     }
 
     private fun isBlocked(identity: String): Boolean =
-        blackListService.has(identity) || contactService.getByIdentity(identity) == null && preferenceService.isBlockUnknown
+        blockedContactsService.has(identity) || contactService.getByIdentity(identity) == null && preferenceService.isBlockUnknown
 
     private class DiscardMessageException(
         val discardedMessage: AbstractMessage?,

+ 3 - 3
app/src/main/java/ch/threema/app/processors/groupcontrol/IncomingGroupSetupTask.kt

@@ -48,7 +48,7 @@ class IncomingGroupSetupTask(
     private val userService = serviceManager.userService
     private val contactService = serviceManager.contactService
     private val groupService = serviceManager.groupService
-    private val blackListService = serviceManager.blackListService
+    private val blockedContactsService = serviceManager.blockedContactsService
     private val groupCallManager = serviceManager.groupCallManager
     private val databaseService = serviceManager.databaseServiceNew
     private val contactStore = serviceManager.contactStore
@@ -184,7 +184,7 @@ class IncomingGroupSetupTask(
             identityStore,
             contactStore,
             nonceFactory,
-            blackListService,
+            blockedContactsService,
             taskCreator
         )
     }
@@ -226,6 +226,6 @@ class IncomingGroupSetupTask(
     }
 
     private fun isBlocked(identity: String): Boolean =
-        blackListService.has(identity) ||
+        blockedContactsService.has(identity) ||
                 (contactService.getByIdentity(identity) == null && preferenceService.isBlockUnknown)
 }

+ 26 - 29
app/src/main/java/ch/threema/app/routines/SynchronizeContactsRoutine.java

@@ -47,7 +47,6 @@ import ch.threema.app.services.IdListService;
 import ch.threema.app.services.LocaleService;
 import ch.threema.app.services.PreferenceService;
 import ch.threema.app.services.UserService;
-import ch.threema.app.services.license.LicenseService;
 import ch.threema.app.stores.MatchTokenStore;
 import ch.threema.app.utils.AndroidContactUtil;
 import ch.threema.app.utils.ConfigUtils;
@@ -74,12 +73,11 @@ public class SynchronizeContactsRoutine implements Runnable {
 	private final DeviceService deviceService;
 	private final PreferenceService preferenceService;
 	private final IdentityStoreInterface identityStore;
-	private final IdListService blackListIdentityService;
-	private final LicenseService<?> licenseService;
+	private final IdListService blockedContactsService;
 
 	private OnStatusUpdate onStatusUpdate;
-	private final List<OnFinished> onFinished = new ArrayList<OnFinished>();
-	private final List<OnStarted> onStarted = new ArrayList<OnStarted>();
+	private final List<OnFinished> onFinished = new ArrayList<>();
+	private final List<OnStarted> onStarted = new ArrayList<>();
 
 	private final List<String> processingIdentities = new ArrayList<>();
 	private boolean abort = false;
@@ -99,18 +97,19 @@ public class SynchronizeContactsRoutine implements Runnable {
 		void started(boolean fullSync);
 	}
 
-	public SynchronizeContactsRoutine(Context context,
-	                                  APIConnector apiConnector,
-	                                  ContactService contactService,
-	                                  UserService userService,
-	                                  LocaleService localeService,
-	                                  ContentResolver contentResolver,
-	                                  IdListService excludedSyncList,
-	                                  DeviceService deviceService,
-	                                  PreferenceService preferenceService,
-	                                  IdentityStoreInterface identityStore,
-	                                  IdListService blackListIdentityService,
-	                                  LicenseService<?> licenseService) {
+    public SynchronizeContactsRoutine(
+        Context context,
+        APIConnector apiConnector,
+        ContactService contactService,
+        UserService userService,
+        LocaleService localeService,
+        ContentResolver contentResolver,
+        IdListService excludedSyncList,
+        DeviceService deviceService,
+        PreferenceService preferenceService,
+        IdentityStoreInterface identityStore,
+        IdListService blockedContactsService
+    ) {
 		this.context = context;
 		this.apiConnector = apiConnector;
 		this.userService = userService;
@@ -121,8 +120,7 @@ public class SynchronizeContactsRoutine implements Runnable {
 		this.deviceService = deviceService;
 		this.preferenceService = preferenceService;
 		this.identityStore = identityStore;
-		this.licenseService = licenseService;
-		this.blackListIdentityService = blackListIdentityService;
+		this.blockedContactsService = blockedContactsService;
 	}
 
 	public SynchronizeContactsRoutine addProcessIdentity(String identity) {
@@ -141,7 +139,7 @@ public class SynchronizeContactsRoutine implements Runnable {
 	}
 
 	public boolean fullSync() {
-		return this.processingIdentities.size() == 0;
+		return this.processingIdentities.isEmpty();
 	}
 
 	@Override
@@ -243,7 +241,7 @@ public class SynchronizeContactsRoutine implements Runnable {
 					continue;
 				}
 
-				if(this.processingIdentities.size() > 0 && !this.processingIdentities.contains(id.getKey())) {
+				if(!this.processingIdentities.isEmpty() && !this.processingIdentities.contains(id.getKey())) {
 					continue;
 				}
 
@@ -282,7 +280,7 @@ public class SynchronizeContactsRoutine implements Runnable {
 				contact.setAndroidContactLookupKey(lookupKey + "/" + contactId); // It can optionally also have a "/" and last known contact ID appended after that. This "complete" format is an important optimization and is highly recommended.
 
 				try {
-					boolean createNewRawContact = false;
+					boolean createNewRawContact;
 
 					AndroidContactUtil.getInstance().updateNameByAndroidContact(contact); // throws an exception if no name can be determined
 					AndroidContactUtil.getInstance().updateAvatarByAndroidContact(contact);
@@ -294,7 +292,7 @@ public class SynchronizeContactsRoutine implements Runnable {
 
 					List<AndroidContactUtil.RawContactInfo> rawContactInfos = existingRawContacts.get(contact.getIdentity());
 
-					if (rawContactInfos == null || rawContactInfos.size() == 0) {
+					if (rawContactInfos.isEmpty()) {
 						// raw contact does not exist yet, create it
 						createNewRawContact = true;
 						newRawContactCount++;
@@ -315,7 +313,7 @@ public class SynchronizeContactsRoutine implements Runnable {
 					}
 
 					if (createNewRawContact) {
-						boolean supportsVoiceCalls = ContactUtil.canReceiveVoipMessages(contact, this.blackListIdentityService)
+						boolean supportsVoiceCalls = ContactUtil.canReceiveVoipMessages(contact, this.blockedContactsService)
 							&& ConfigUtils.isCallsEnabled();
 
 						// create a raw contact for our stuff and aggregate it
@@ -342,7 +340,7 @@ public class SynchronizeContactsRoutine implements Runnable {
 				}
 			}
 
-			if (contentProviderOperations.size() > 0) {
+			if (!contentProviderOperations.isEmpty()) {
 				try {
 					ConfigUtils.applyToContentResolverInBatches(
 						ContactsContract.AUTHORITY,
@@ -355,12 +353,12 @@ public class SynchronizeContactsRoutine implements Runnable {
 
 			if (this.fullSync()) {
 				// delete remaining / stray raw contacts
-				if (existingRawContacts.size() > 0) {
+				if (!existingRawContacts.isEmpty()) {
 					AndroidContactUtil.getInstance().deleteThreemaRawContacts(existingRawContacts);
 					logger.info("Deleted {} stray raw contacts", existingRawContacts.size());
 				}
 
-				if (preSynchronizedIdentities.size() > 0) {
+				if (!preSynchronizedIdentities.isEmpty()) {
 					logger.info("Found {} synchronized contacts that are no longer synchronized", preSynchronizedIdentities.size());
 
 					List<ContactModel> contactModels = this.contactService.getByIdentities(preSynchronizedIdentities);
@@ -449,9 +447,8 @@ public class SynchronizeContactsRoutine implements Runnable {
 
 	private Map<String, ContactMatchKeyEmail> readEmails() {
 		Map<String, ContactMatchKeyEmail> emails = new HashMap<>();
-		String selection = null;
 
-		selection = ContactsContract.CommonDataKinds.Email.IN_DEFAULT_DIRECTORY + " = 1";
+        String selection = ContactsContract.CommonDataKinds.Email.IN_DEFAULT_DIRECTORY + " = 1";
 
 		try (Cursor emailsCursor = this.contentResolver.query(
 			ContactsContract.CommonDataKinds.Email.CONTENT_URI,

+ 4 - 4
app/src/main/java/ch/threema/app/services/ContactServiceImpl.java

@@ -141,7 +141,7 @@ public class ContactServiceImpl implements ContactService {
     // NOTE: The contact model cache will become unnecessary once everything uses the new data
     // layer, since that data layer has caching built-in.
     private final Map<String, ContactModel> contactModelCache;
-    private final IdListService blackListIdentityService, profilePicRecipientsService;
+    private final IdListService blockedContactsService, profilePicRecipientsService;
     private final DeadlineListService mutedChatsListService;
     private final DeadlineListService hiddenChatsListService;
     private final RingtoneService ringtoneService;
@@ -185,7 +185,7 @@ public class ContactServiceImpl implements ContactService {
         UserService userService,
         IdentityStore identityStore,
         PreferenceService preferenceService,
-        IdListService blackListIdentityService,
+        IdListService blockedContactsService,
         IdListService profilePicRecipientsService,
         RingtoneService ringtoneService,
         DeadlineListService mutedChatsListService,
@@ -207,7 +207,7 @@ public class ContactServiceImpl implements ContactService {
         this.userService = userService;
         this.identityStore = identityStore;
         this.preferenceService = preferenceService;
-        this.blackListIdentityService = blackListIdentityService;
+        this.blockedContactsService = blockedContactsService;
         this.profilePicRecipientsService = profilePicRecipientsService;
         this.ringtoneService = ringtoneService;
         this.mutedChatsListService = mutedChatsListService;
@@ -1119,7 +1119,7 @@ public class ContactServiceImpl implements ContactService {
             serviceManager,
             this.databaseServiceNew,
             this.identityStore,
-            this.blackListIdentityService
+            this.blockedContactsService
         );
     }
 

+ 4 - 4
app/src/main/java/ch/threema/app/services/ConversationServiceImpl.java

@@ -77,7 +77,7 @@ public class ConversationServiceImpl implements ConversationService {
     private final DistributionListService distributionListService;
     private final MessageService messageService;
     private final DeadlineListService hiddenChatsListService;
-    private final IdListService blackListService;
+    private final IdListService blockedContactsService;
     private boolean initAllLoaded = false;
     private final TagModel unreadTagModel;
     private final @NonNull String pinTag;
@@ -130,7 +130,7 @@ public class ConversationServiceImpl implements ConversationService {
         DistributionListService distributionListService,
         MessageService messageService,
         @NonNull DeadlineListService hiddenChatsListService,
-        @NonNull IdListService blackListService,
+        @NonNull IdListService blockedContactsService,
         ConversationTagService conversationTagService) {
         this.context = context;
         this.databaseServiceNew = databaseServiceNew;
@@ -139,7 +139,7 @@ public class ConversationServiceImpl implements ConversationService {
         this.distributionListService = distributionListService;
         this.messageService = messageService;
         this.hiddenChatsListService = hiddenChatsListService;
-        this.blackListService = blackListService;
+        this.blockedContactsService = blockedContactsService;
         this.conversationCache = cacheService.getConversationModelCache();
         this.conversationTagService = conversationTagService;
 
@@ -267,7 +267,7 @@ public class ConversationServiceImpl implements ConversationService {
                         public boolean apply(@NonNull ConversationModel conversationModel) {
                             if (conversationModel.isContactConversation()) {
                                 return !ContactUtil.isEchoEchoOrGatewayContact(conversationModel.getContact()) &&
-                                    !blackListService.has(conversationModel.getContact().getIdentity());
+                                    !blockedContactsService.has(conversationModel.getContact().getIdentity());
                             }
                             return true;
                         }

+ 4 - 4
app/src/main/java/ch/threema/app/services/MessageServiceImpl.java

@@ -220,7 +220,7 @@ public class MessageServiceImpl implements MessageService {
 	private final ApiService apiService;
 	private final DownloadService downloadService;
 	private final DeadlineListService hiddenChatsListService;
-	private final IdListService blackListService;
+	private final IdListService blockedContactsService;
 	private final SymmetricEncryptionService symmetricEncryptionService;
 
     // Repositories
@@ -248,7 +248,7 @@ public class MessageServiceImpl implements MessageService {
 	    ApiService apiService,
 	    DownloadService downloadService,
 	    DeadlineListService hiddenChatsListService,
-	    IdListService blackListService,
+	    IdListService blockedContactsService,
         EditHistoryRepository editHistoryRepository
 	) {
 		this.context = context;
@@ -264,7 +264,7 @@ public class MessageServiceImpl implements MessageService {
 		this.apiService = apiService;
 		this.downloadService = downloadService;
 		this.hiddenChatsListService = hiddenChatsListService;
-		this.blackListService = blackListService;
+		this.blockedContactsService = blockedContactsService;
 
 		contactMessageCache = cacheService.getMessageModelCache();
 		groupMessageCache = cacheService.getGroupMessageModelCache();
@@ -1482,7 +1482,7 @@ public class MessageServiceImpl implements MessageService {
 		}
 
 		// is the user blocked?
-		if (blackListService != null && blackListService.has(message.getFromIdentity())) {
+		if (blockedContactsService != null && blockedContactsService.has(message.getFromIdentity())) {
 			//set success to true (remove from server)
 			logger.info("GroupMessage {}: Sender is blocked, ignoring", message.getMessageId());
 			return true;

+ 50 - 12
app/src/main/java/ch/threema/app/services/RingtoneService.java

@@ -24,23 +24,61 @@ package ch.threema.app.services;
 import android.content.Context;
 import android.net.Uri;
 
+/**
+ * The ringtone service provides either default or custom ringtones for contacts and groups. Note
+ * that the ringtone manager only manages notification sounds until api 25. From api 26 on, the
+ * ringtone manager won't return any custom notification sounds.
+ */
 public interface RingtoneService {
 
-	void init();
-	void setRingtone(String uniqueId, Uri ringtoneUri);
+    void init();
+
+    void setRingtone(String uniqueId, Uri ringtoneUri);
+
+    /**
+     * Get the ringtone uri from the given unique id. Note that this method returns null on api 26
+     * and newer.
+     */
+    Uri getRingtoneFromUniqueId(String uniqueId);
+
+    /**
+     * Get the ringtone uri from the given unique id. Note that this method returns null on api 26
+     * and newer.
+     */
+    Uri getContactRingtone(String uniqueId);
+
+    /**
+     * Get the ringtone uri from the given unique id. Note that this method returns null on api 26
+     * and newer.
+     */
+    Uri getGroupRingtone(String uniqueId);
+
+    /**
+     * Get the voice call ringtone. Note that this method returns null on api 26 and newer.
+     */
+    Uri getVoiceCallRingtone(String uniqueId);
+
+    /**
+     * Get the default ringtone for contacts. Note that this method returns null on api 26 and
+     * newer.
+     */
+    Uri getDefaultContactRingtone();
 
-	Uri getRingtoneFromUniqueId(String uniqueId);
-	Uri getContactRingtone(String uniqueId);
-	Uri getGroupRingtone(String uniqueId);
-	Uri getVoiceCallRingtone(String uniqueId);
+    /**
+     * Get the default ringtone for groups. Note that this method returns null on api 26 and newer.
+     */
+    Uri getDefaultGroupRingtone();
 
-	Uri getDefaultContactRingtone();
-	Uri getDefaultGroupRingtone();
+    /**
+     * Check whether the given conversation is silent or not. Note that starting from api 26, this
+     * method always returns false as the sound is managed by the system notification channel
+     * settings.
+     */
+    boolean isSilent(String uniqueId, boolean isGroup);
 
-	boolean isSilent(String uniqueId, boolean isGroup);
+    boolean hasCustomRingtone(String uniqueId);
 
-	boolean hasCustomRingtone(String uniqueId);
-	void removeCustomRingtone(String uniqueId);
+    void removeCustomRingtone(String uniqueId);
 
-	void resetRingtones(Context context);
+    void resetRingtones(Context context);
 }

+ 44 - 6
app/src/main/java/ch/threema/app/services/RingtoneServiceImpl.java

@@ -24,17 +24,24 @@ package ch.threema.app.services;
 import android.content.Context;
 import android.media.RingtoneManager;
 import android.net.Uri;
+
+import org.slf4j.Logger;
+
 import androidx.core.app.NotificationCompat;
 
 import java.util.HashMap;
 
 import ch.threema.app.R;
+import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.RingtoneUtil;
 import ch.threema.app.utils.TestUtil;
+import ch.threema.base.utils.LoggingUtil;
 
 public class RingtoneServiceImpl implements RingtoneService {
+    private final static Logger logger = LoggingUtil.getThreemaLogger("RingtoneServiceImpl");
 	private final PreferenceService preferenceService;
 	private HashMap<String, String> ringtones;
+    private final boolean supportsNotificationChannels = ConfigUtils.supportsNotificationChannels();
 
 	public RingtoneServiceImpl(PreferenceService preferenceService) {
 		this.preferenceService = preferenceService;
@@ -44,11 +51,24 @@ public class RingtoneServiceImpl implements RingtoneService {
 
 	@Override
 	public void init() {
-		ringtones = preferenceService.getRingtones();
+        if (supportsNotificationChannels) {
+            // Set empty hash map as notification channels are supported and therefore ringtones
+            // won't be managed by us.
+            HashMap<String, String> emptyRingtones = new HashMap<>();
+            preferenceService.setRingtones(emptyRingtones);
+            ringtones = emptyRingtones;
+        } else {
+            ringtones = preferenceService.getRingtones();
+        }
 	}
 
 	@Override
 	public void setRingtone(String uniqueId, Uri ringtoneUri) {
+        if (supportsNotificationChannels) {
+            logger.error("Cannot set ringtone if notification channels are supported");
+            return;
+        }
+
 		String ringtone = null;
 
 		if (ringtoneUri != null) {
@@ -83,6 +103,10 @@ public class RingtoneServiceImpl implements RingtoneService {
 
 	@Override
 	public void removeCustomRingtone(String uniqueId) {
+        if (supportsNotificationChannels) {
+            logger.warn("No need to remove custom ringtone if notification channels are supported");
+        }
+
 		if (ringtones != null && ringtones.containsKey(uniqueId)) {
 			ringtones.remove(uniqueId);
 
@@ -127,21 +151,30 @@ public class RingtoneServiceImpl implements RingtoneService {
 
 	@Override
 	public Uri getDefaultContactRingtone() {
+        if (supportsNotificationChannels) {
+            return null;
+        }
+
 		return preferenceService.getNotificationSound();
 	}
 
 	@Override
 	public Uri getDefaultGroupRingtone() {
-		return preferenceService.getGroupNotificationSound();
-	}
+        if (supportsNotificationChannels) {
+            return null;
+        }
 
-	private boolean hasNoRingtone(String uniqueId) {
-		Uri ringtone = getRingtoneFromUniqueId(uniqueId);
-		return (ringtone == null || ringtone.toString().equals("null"));
+		return preferenceService.getGroupNotificationSound();
 	}
 
 	@Override
 	public boolean isSilent(String uniqueId, boolean isGroup) {
+        if (supportsNotificationChannels) {
+            // Note that we do not manage the sound of notifications if notification channels are
+            // supported. Therefore we always return false as we do not display this particularly.
+            return false;
+        }
+
 		if (!TestUtil.isEmptyOrNull(uniqueId)) {
 			Uri defaultRingtone, selectedRingtone;
 
@@ -156,4 +189,9 @@ public class RingtoneServiceImpl implements RingtoneService {
 		}
 		return false;
 	}
+
+    private boolean hasNoRingtone(String uniqueId) {
+        Uri ringtone = getRingtoneFromUniqueId(uniqueId);
+        return (ringtone == null || ringtone.toString().equals("null"));
+    }
 }

+ 16 - 21
app/src/main/java/ch/threema/app/services/SynchronizeContactsServiceImpl.java

@@ -41,7 +41,6 @@ import ch.threema.app.listeners.SynchronizeContactsListener;
 import ch.threema.app.managers.ListenerManager;
 import ch.threema.app.routines.SynchronizeContactsRoutine;
 import ch.threema.app.routines.UpdateBusinessAvatarRoutine;
-import ch.threema.app.services.license.LicenseService;
 import ch.threema.app.utils.AndroidContactUtil;
 import ch.threema.app.utils.ContactUtil;
 import ch.threema.base.utils.LoggingUtil;
@@ -68,8 +67,7 @@ public class SynchronizeContactsServiceImpl implements SynchronizeContactsServic
 	private final DeviceService deviceService;
 	private final Context context;
 	private final FileService fileService;
-	private final IdListService blackListIdentityService;
-	private final LicenseService licenseService;
+	private final IdListService blockedContactsService;
 	private final ApiService apiService;
 
 	private Date latestFullSync;
@@ -84,8 +82,7 @@ public class SynchronizeContactsServiceImpl implements SynchronizeContactsServic
 										  DeviceService deviceService,
 										  FileService fileService,
 	                                      IdentityStoreInterface identityStore,
-	                                      IdListService blackListIdentityService,
-	                                      LicenseService<LicenseService.Credentials> licenseService,
+	                                      IdListService blockedContactsService,
 	                                      ApiService apiService) {
 		this.excludedIdentityListService = excludedIdentityListService;
 		this.preferenceService = preferenceService;
@@ -99,8 +96,7 @@ public class SynchronizeContactsServiceImpl implements SynchronizeContactsServic
 		this.userService = userService;
 		this.localeService = localeService;
 		this.identityStore = identityStore;
-		this.licenseService = licenseService;
-		this.blackListIdentityService = blackListIdentityService;
+		this.blockedContactsService = blockedContactsService;
 		this.apiService = apiService;
 	}
 
@@ -184,20 +180,19 @@ public class SynchronizeContactsServiceImpl implements SynchronizeContactsServic
 		logger.info("Running contact sync");
 		logger.debug("instantiateSynchronization with account {}", account);
 
-		final SynchronizeContactsRoutine routine =
-				new SynchronizeContactsRoutine(
-						this.context,
-						this.apiConnector,
-						this.contactService,
-						this.userService,
-						this.localeService,
-						this.contentResolver,
-						this.excludedIdentityListService,
-						this.deviceService,
-						this.preferenceService,
-						this.identityStore,
-						this.blackListIdentityService,
-						this.licenseService);
+        final SynchronizeContactsRoutine routine = new SynchronizeContactsRoutine(
+            this.context,
+            this.apiConnector,
+            this.contactService,
+            this.userService,
+            this.localeService,
+            this.contentResolver,
+            this.excludedIdentityListService,
+            this.deviceService,
+            this.preferenceService,
+            this.identityStore,
+            this.blockedContactsService
+        );
 
 		synchronized (this.pendingRoutines) {
 			this.pendingRoutines.add(routine);

+ 2 - 2
app/src/main/java/ch/threema/app/tasks/OutgoingCspGroupControlMessageTask.kt

@@ -35,7 +35,7 @@ import java.util.Date
 abstract class OutgoingCspGroupControlMessageTask(serviceManager: ServiceManager) :
     OutgoingCspMessageTask(serviceManager) {
     private val taskCreator by lazy { serviceManager.taskCreator }
-    private val blackListService by lazy { serviceManager.blackListService }
+    private val blockedContactsService by lazy { serviceManager.blockedContactsService }
 
     protected abstract val messageId: MessageId
     protected abstract val creatorIdentity: String
@@ -65,7 +65,7 @@ abstract class OutgoingCspGroupControlMessageTask(serviceManager: ServiceManager
             identityStore,
             contactStore,
             nonceFactory,
-            blackListService,
+            blockedContactsService,
             taskCreator
         )
     }

+ 3 - 3
app/src/main/java/ch/threema/app/tasks/OutgoingCspMessageTask.kt

@@ -69,7 +69,7 @@ sealed class OutgoingCspMessageTask(serviceManager: ServiceManager) :
     // It is important that the task creator is loaded lazily, as the task archiver may instantiate
     // this class before the connection is initialized (which is used for the task creator).
     private val taskCreator by lazy { serviceManager.taskCreator }
-    private val blackListService by lazy { serviceManager.blackListService }
+    private val blockedContactsService by lazy { serviceManager.blockedContactsService }
 
     final override suspend fun invoke(handle: ActiveTaskCodec) {
         suspend {
@@ -140,7 +140,7 @@ sealed class OutgoingCspMessageTask(serviceManager: ServiceManager) :
                 identityStore,
                 contactStore,
                 nonceFactory,
-                blackListService,
+                blockedContactsService,
                 taskCreator
             )
         }.catchExceptNetworkException { e: BadDHStateException ->
@@ -211,7 +211,7 @@ sealed class OutgoingCspMessageTask(serviceManager: ServiceManager) :
                 contactStore,
                 nonceFactory,
                 groupService,
-                blackListService,
+                blockedContactsService,
                 taskCreator
             )
         }.catchExceptNetworkException { e: BadDHStateException ->

+ 2 - 2
app/src/main/java/ch/threema/app/tasks/OutgoingGroupSyncRequestTask.kt

@@ -56,7 +56,7 @@ class OutgoingGroupSyncRequestTask(
     private val apiConnector by lazy { serviceManager.apiConnector }
     private val taskCreator by lazy { serviceManager.taskCreator }
     private val outgoingGroupSyncRequestLogModelFactory by lazy { serviceManager.databaseServiceNew.outgoingGroupSyncRequestLogModelFactory }
-    private val blackListService by lazy { serviceManager.blackListService }
+    private val blockedContactsService by lazy { serviceManager.blockedContactsService }
 
     override val type: String = "OutgoingGroupSyncRequestTask"
 
@@ -96,7 +96,7 @@ class OutgoingGroupSyncRequestTask(
             identityStore,
             contactStore,
             nonceFactory,
-            blackListService,
+            blockedContactsService,
             taskCreator
         )
 

+ 5 - 5
app/src/main/java/ch/threema/app/threemasafe/ThreemaSafeServiceImpl.java

@@ -238,7 +238,7 @@ public class ThreemaSafeServiceImpl implements ThreemaSafeService {
 	private final GroupService groupService;
 	private final DistributionListService distributionListService;
 	private final FileService fileService;
-	private final IdListService blackListService;
+	private final IdListService blockedContactsService;
 	private final IdListService excludedSyncIdentitiesService;
 	private final IdListService profilePicRecipientsService;
 	private final DatabaseServiceNew databaseServiceNew;
@@ -257,7 +257,7 @@ public class ThreemaSafeServiceImpl implements ThreemaSafeService {
 		DistributionListService distributionListService,
 		LocaleService localeService,
 		FileService fileService,
-		IdListService blackListService,
+		IdListService blockedContactsService,
 		IdListService excludedSyncIdentitiesService,
 		IdListService profilePicRecipientsService,
 		DatabaseServiceNew databaseServiceNew,
@@ -280,7 +280,7 @@ public class ThreemaSafeServiceImpl implements ThreemaSafeService {
 		this.localeService = localeService;
 		this.databaseServiceNew = databaseServiceNew;
 		this.fileService = fileService;
-		this.blackListService = blackListService;
+		this.blockedContactsService = blockedContactsService;
 		this.excludedSyncIdentitiesService = excludedSyncIdentitiesService;
 		this.profilePicRecipientsService = profilePicRecipientsService;
 		this.hiddenChatsListService = hiddenChatsListService;
@@ -1554,7 +1554,7 @@ public class ThreemaSafeServiceImpl implements ThreemaSafeService {
 	private JSONArray getSettingsBlockedContacts() {
 		JSONArray blockedContactsArray = new JSONArray();
 
-		for (final String id : blackListService.getAll()) {
+		for (final String id : blockedContactsService.getAll()) {
 			blockedContactsArray.put(id);
 		}
 
@@ -1566,7 +1566,7 @@ public class ThreemaSafeServiceImpl implements ThreemaSafeService {
 
 		for (int i=0; i <blockedContacts.length(); i++) {
 			try {
-				blackListService.add(blockedContacts.getString(i));
+				blockedContactsService.add(blockedContacts.getString(i));
 			} catch (JSONException e) {
 				// ignore invalid entry
 			}

+ 1 - 1
app/src/main/java/ch/threema/app/ui/AudioProgressBarView.kt

@@ -93,7 +93,7 @@ class AudioProgressBarView : androidx.appcompat.widget.AppCompatSeekBar, AudioWa
     }
 
     fun init(attrs: AttributeSet?) {
-        barColor = ContextCompat.getColorStateList(context, R.color.bubble_text_colorstatelist)!!
+        barColor = ContextCompat.getColorStateList(context, R.color.bubble_send_text_colorstatelist)!!
         val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.AudioProgressBarView, 0, 0)
 
         with(typedArray) {

+ 5 - 7
app/src/main/java/ch/threema/app/ui/AvatarEditView.java

@@ -55,7 +55,6 @@ import androidx.annotation.RequiresApi;
 import androidx.annotation.UiThread;
 import androidx.annotation.WorkerThread;
 import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.view.ContextThemeWrapper;
 import androidx.appcompat.view.menu.MenuBuilder;
 import androidx.appcompat.view.menu.MenuPopupHelper;
 import androidx.core.app.ActivityCompat;
@@ -399,7 +398,7 @@ public class AvatarEditView extends FrameLayout implements DefaultLifecycleObser
 
 	@SuppressLint("RestrictedApi")
 	@Override
-	public void onClick(View v) {
+	public void onClick(View view) {
 		if (!isAvatarEditable()) {
 			return;
 		}
@@ -407,7 +406,7 @@ public class AvatarEditView extends FrameLayout implements DefaultLifecycleObser
 		MenuBuilder menuBuilder = new MenuBuilder(getContext());
 		new MenuInflater(getContext()).inflate(R.menu.view_avatar_edit, menuBuilder);
 
-		ConfigUtils.tintMenu(menuBuilder, ConfigUtils.getColorFromAttribute(getContext(), R.attr.colorOnSurface));
+		ConfigUtils.tintMenuIcons(menuBuilder, ConfigUtils.getColorFromAttribute(getContext(), R.attr.colorOnSurface));
 
 		if (!hasAvatar()) {
 			menuBuilder.removeItem(R.id.menu_remove_picture);
@@ -436,11 +435,10 @@ public class AvatarEditView extends FrameLayout implements DefaultLifecycleObser
 			}
 		});
 
-		Context wrapper = new ContextThemeWrapper(getContext(), R.style.AppBaseTheme);
-		MenuPopupHelper optionsMenu = new MenuPopupHelper(wrapper, menuBuilder, avatarEditOverlay);
+		MenuPopupHelper optionsMenu = new MenuPopupHelper(getContext(), menuBuilder, avatarEditOverlay);
 		optionsMenu.setForceShowIcon(true);
-		optionsMenu.show();
-	}
+        optionsMenu.show();
+    }
 
 	@Override
 	public boolean onLongClick(View v) {

+ 281 - 188
app/src/main/java/ch/threema/app/ui/ControllerView.java

@@ -37,200 +37,293 @@ import org.slf4j.Logger;
 
 import androidx.annotation.ColorInt;
 import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 import androidx.appcompat.widget.AppCompatImageView;
 
 import ch.threema.app.R;
+import ch.threema.app.utils.ColorUtil;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.base.utils.LoggingUtil;
 
 public class ControllerView extends MaterialCardView {
-	private static final Logger logger = LoggingUtil.getThreemaLogger("ControllerView");
-
-	private CircularProgressIndicator progressBarIndeterminate, progressBarDeterminate;
-	private AppCompatImageView icon;
-	private @ColorInt int foregroundColor;
-	private int status, progressMax = 100;
-
-	public final static int STATUS_NONE = 0;
-	public final static int STATUS_PROGRESSING = 1;
-	public final static int STATUS_READY_TO_DOWNLOAD = 2;
-	public final static int STATUS_READY_TO_PLAY = 3;
-	public final static int STATUS_PLAYING = 4;
-	public final static int STATUS_READY_TO_RETRY = 5;
-	public final static int STATUS_PROGRESSING_NO_CANCEL = 6;
-	public final static int STATUS_TRANSCODING = 8;
-
-	public ControllerView(Context context) {
-		super(context);
-		init(context);
-	}
-
-	public ControllerView(Context context, AttributeSet attrs) {
-		super(context, attrs);
-		init(context);
-	}
-
-	public ControllerView(Context context, AttributeSet attrs, int defStyleAttr) {
-		super(context, attrs, defStyleAttr);
-		init(context);
-	}
-
-	private void init(Context context) {
-		LayoutInflater inflater = (LayoutInflater) context
-				.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-		inflater.inflate(R.layout.conversation_list_item_controller_view, this);
-
-		this.setShapeAppearanceModel(ShapeAppearanceModel.builder(context, 0, R.style.ShapeAppearance_Material3_Corner_Medium).build());
-		this.setStrokeWidth(0);
-		this.foregroundColor = ConfigUtils.getColorFromAttribute(context, R.attr.colorOnBackground);
-	}
-
-	@Override
-	protected void onFinishInflate() {
-		super.onFinishInflate();
-
-		progressBarIndeterminate = this.findViewById(R.id.progress);
-		progressBarDeterminate = this.findViewById(R.id.progress_determinate);
-		icon = this.findViewById(R.id.icon);
-
-		setBackgroundImage(null);
-	}
-
-	private void reset() {
-		if (getVisibility() != VISIBLE) {
-			setVisibility(VISIBLE);
-		}
-		if (progressBarIndeterminate.getVisibility() == VISIBLE) {
-			progressBarIndeterminate.setVisibility(INVISIBLE);
-		}
-		if (progressBarDeterminate.getVisibility() == VISIBLE) {
-			progressBarDeterminate.setVisibility(INVISIBLE);
-		}
-		if (icon.getVisibility() != VISIBLE) {
-			icon.setVisibility(VISIBLE);
-		}
-	}
-
-	public void setNeutral() {
-		logger.debug("setNeutral");
-		reset();
-		status = STATUS_NONE;
-	}
-
-	public void setHidden() {
-		logger.debug("setHidden");
-		setVisibility(INVISIBLE);
-		status = STATUS_NONE;
-	}
-
-	@UiThread
-	public void setPlay() {
-		logger.debug("setPlay");
-		if (status != STATUS_READY_TO_PLAY) {
-			setImageResource(R.drawable.ic_play);
-			setContentDescription(getContext().getString(R.string.play));
-			status = STATUS_READY_TO_PLAY;
-		}
-	}
-
-	@UiThread
-	public void setPause() {
-		logger.debug("setPause");
-		setImageResource(R.drawable.ic_pause);
-		setContentDescription(getContext().getString(R.string.pause));
-		status = STATUS_PLAYING;
-	}
-
-	public void setTranscoding() {
-		setHidden();
-		status = STATUS_TRANSCODING;
-	}
-
-	public void setProgressing() {
-		setProgressing(true);
-	}
-
-	@UiThread
-	public void setProgressing(boolean cancelable) {
-		logger.debug("setProgressing cancelable = " + cancelable);
-		if (progressBarIndeterminate.getVisibility() != VISIBLE) {
-			reset();
-			if (cancelable) {
-				if (status != STATUS_PROGRESSING) {
-					setImageResource(R.drawable.ic_close);
-					status = STATUS_PROGRESSING;
-				}
-			} else {
-				if (status != STATUS_PROGRESSING_NO_CANCEL) {
-					icon.setVisibility(INVISIBLE);
-					status = STATUS_PROGRESSING_NO_CANCEL;
-				}
-			}
-			progressBarIndeterminate.setVisibility(VISIBLE);
-		} else {
-			setVisibility(VISIBLE);
-			if (cancelable) {
-				status = STATUS_PROGRESSING;
-			} else {
-				status = STATUS_PROGRESSING_NO_CANCEL;
-			}
-		}
-		requestLayout();
-	}
-
-	public void setProgressingDeterminate(int max) {
-		logger.debug("setProgressingDeterminate max = " + max);
-		setBackgroundImage(null);
-		setImageResource(R.drawable.ic_close);
-		setContentDescription(getContext().getString(R.string.cancel));
-		progressBarDeterminate.setMax(max);
-		progressBarDeterminate.setProgress(0);
-		progressBarDeterminate.setVisibility(VISIBLE);
-		status = STATUS_PROGRESSING;
-		progressMax = max;
-	}
-
-	public void setProgress(int progress) {
-		logger.debug("setProgress progress = " + progress);
-		if (progressBarDeterminate.getVisibility() != VISIBLE) {
-			setProgressingDeterminate(progressMax);
-		}
-		progressBarDeterminate.setProgress(progress);
-	}
-
-	public void setRetry() {
-		logger.debug("setRetry");
-		setImageResource(R.drawable.outline_refresh_24);
-		setContentDescription(getContext().getString(R.string.retry));
-		status = STATUS_READY_TO_RETRY;
-	}
-
-	public void setReadyToDownload() {
-		logger.debug("setReadyToDownload");
-		setImageResource(R.drawable.outline_file_download_24);
-		setContentDescription(getContext().getString(R.string.download));
-		status = STATUS_READY_TO_DOWNLOAD;
-	}
-
-	public void setImageResource(@DrawableRes int resource) {
-		logger.debug("setImageResource");
-		reset();
-		icon.setColorFilter(foregroundColor, PorterDuff.Mode.SRC_IN);
-		icon.setScaleType(ImageView.ScaleType.CENTER);
-		icon.setImageResource(resource);
-	}
-
-	public void setBackgroundImage(Bitmap bitmap) {
-		logger.debug("setBackgroundImage");
-		if (bitmap != null) {
-			icon.clearColorFilter();
-			icon.setScaleType(ImageView.ScaleType.CENTER_CROP);
-			icon.setImageDrawable(new BitmapDrawable(getResources(), bitmap));
-		}
-	}
-
-	public int getStatus() {
-		return status;
-	}
+    private static final Logger logger = LoggingUtil.getThreemaLogger("ControllerView");
+
+    private CircularProgressIndicator progressBarIndeterminate, progressBarDeterminate;
+    private AppCompatImageView iconImageView;
+    private int status, progressMax = 100;
+
+    private boolean hasBackgroundImage = false;
+    private boolean isUsedForOutboxMessage = false;
+
+    public final static int STATUS_NONE = 0;
+    public final static int STATUS_PROGRESSING = 1;
+    public final static int STATUS_READY_TO_DOWNLOAD = 2;
+    public final static int STATUS_READY_TO_PLAY = 3;
+    public final static int STATUS_PLAYING = 4;
+    public final static int STATUS_READY_TO_RETRY = 5;
+    public final static int STATUS_PROGRESSING_NO_CANCEL = 6;
+    public final static int STATUS_TRANSCODING = 8;
+
+    public ControllerView(Context context) {
+        super(context);
+        init(context);
+    }
+
+    public ControllerView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    public ControllerView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(context);
+    }
+
+    private void init(@NonNull Context context) {
+        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        inflater.inflate(R.layout.conversation_list_item_controller_view, this);
+
+        this.setCardBackgroundColor(getResources().getColor(android.R.color.transparent));
+        this.setShapeAppearanceModel(ShapeAppearanceModel.builder(context, 0, R.style.ShapeAppearance_Material3_Corner_Medium).build());
+        this.setStrokeWidth(0);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        progressBarIndeterminate = this.findViewById(R.id.progress);
+        progressBarDeterminate = this.findViewById(R.id.progress_determinate);
+        iconImageView = this.findViewById(R.id.icon);
+
+        setBackgroundImage(null);
+        initProgressBars();
+    }
+
+    private void initProgressBars() {
+        progressBarIndeterminate.setIndicatorColor(getProgressTrackIndicatorColor());
+        progressBarDeterminate.setTrackColor(getProgressTrackColor());
+        progressBarDeterminate.setIndicatorColor(getProgressTrackIndicatorColor());
+    }
+
+    private void reset() {
+        if (getVisibility() != VISIBLE) {
+            setVisibility(VISIBLE);
+        }
+        if (progressBarIndeterminate.getVisibility() == VISIBLE) {
+            progressBarIndeterminate.setVisibility(INVISIBLE);
+        }
+        if (progressBarDeterminate.getVisibility() == VISIBLE) {
+            progressBarDeterminate.setVisibility(INVISIBLE);
+        }
+        if (iconImageView.getVisibility() != VISIBLE) {
+            iconImageView.setVisibility(VISIBLE);
+        }
+    }
+
+    public void setNeutral() {
+        logger.debug("setNeutral");
+        reset();
+        status = STATUS_NONE;
+    }
+
+    public void setHidden() {
+        logger.debug("setHidden");
+        setVisibility(INVISIBLE);
+        status = STATUS_NONE;
+    }
+
+    @UiThread
+    public void setPlay() {
+        logger.debug("setPlay");
+        if (status != STATUS_READY_TO_PLAY) {
+            setIconResource(R.drawable.ic_play);
+            setContentDescription(getContext().getString(R.string.play));
+            status = STATUS_READY_TO_PLAY;
+        }
+    }
+
+    @UiThread
+    public void setPause() {
+        logger.debug("setPause");
+        setIconResource(R.drawable.ic_pause);
+        setContentDescription(getContext().getString(R.string.pause));
+        status = STATUS_PLAYING;
+    }
+
+    public void setTranscoding() {
+        setHidden();
+        status = STATUS_TRANSCODING;
+    }
+
+    public void setProgressing() {
+        setProgressing(true);
+    }
+
+    @UiThread
+    public void setProgressing(boolean cancelable) {
+        logger.debug("setProgressing cancelable = {}", cancelable);
+        if (progressBarIndeterminate.getVisibility() != VISIBLE) {
+            reset();
+            if (cancelable) {
+                if (status != STATUS_PROGRESSING) {
+                    setIconResource(R.drawable.ic_close);
+                    status = STATUS_PROGRESSING;
+                }
+            } else {
+                if (status != STATUS_PROGRESSING_NO_CANCEL) {
+                    iconImageView.setVisibility(INVISIBLE);
+                    status = STATUS_PROGRESSING_NO_CANCEL;
+                }
+            }
+            progressBarIndeterminate.setVisibility(VISIBLE);
+        } else {
+            setVisibility(VISIBLE);
+            if (cancelable) {
+                status = STATUS_PROGRESSING;
+            } else {
+                status = STATUS_PROGRESSING_NO_CANCEL;
+            }
+        }
+        requestLayout();
+    }
+
+    public void setProgressingDeterminate(int max) {
+        logger.debug("setProgressingDeterminate max = {}", max);
+        setBackgroundImage(null);
+        setIconResource(R.drawable.ic_close);
+        setContentDescription(getContext().getString(R.string.cancel));
+        progressBarDeterminate.setMax(max);
+        progressBarDeterminate.setProgress(0);
+        progressBarDeterminate.setVisibility(VISIBLE);
+        status = STATUS_PROGRESSING;
+        progressMax = max;
+    }
+
+    public void setProgress(int progress) {
+        logger.debug("setProgress progress = {}", progress);
+        if (progressBarDeterminate.getVisibility() != VISIBLE) {
+            setProgressingDeterminate(progressMax);
+        }
+        progressBarDeterminate.setProgress(progress);
+    }
+
+    public void setRetry() {
+        logger.debug("setRetry");
+        setIconResource(R.drawable.outline_refresh_24);
+        setContentDescription(getContext().getString(R.string.retry));
+        status = STATUS_READY_TO_RETRY;
+    }
+
+    public void setReadyToDownload() {
+        logger.debug("setReadyToDownload");
+        setIconResource(R.drawable.outline_file_download_24);
+        setContentDescription(getContext().getString(R.string.download));
+        status = STATUS_READY_TO_DOWNLOAD;
+    }
+
+    public void setIconResource(@DrawableRes int resource) {
+        logger.debug("setImageResource");
+        reset();
+        hasBackgroundImage = false;
+        iconImageView.setColorFilter(getIconTintColor(), PorterDuff.Mode.SRC_IN);
+        iconImageView.setScaleType(ImageView.ScaleType.CENTER);
+        iconImageView.setImageResource(resource);
+    }
+
+    public void setBackgroundImage(@Nullable Bitmap bitmap) {
+        logger.debug("setBackgroundImage");
+        hasBackgroundImage = bitmap != null;
+        if (bitmap != null) {
+            iconImageView.clearColorFilter();
+            iconImageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
+            iconImageView.setImageDrawable(new BitmapDrawable(getResources(), bitmap));
+        } else {
+            iconImageView.setBackgroundColor(getBackgroundDefaultColor());
+        }
+    }
+
+    public int getStatus() {
+        return status;
+    }
+
+    public void setIsUsedForOutboxMessage(boolean isOutboxMessage) {
+        if (this.isUsedForOutboxMessage != isOutboxMessage) {
+            this.isUsedForOutboxMessage = isOutboxMessage;
+            if (!hasBackgroundImage) {
+                iconImageView.setBackgroundColor(getBackgroundDefaultColor());
+                iconImageView.setColorFilter(getIconTintColor(), PorterDuff.Mode.SRC_IN);
+            }
+            initProgressBars();
+        }
+    }
+
+    /**
+     * We only want to apply dynamic colors if the current view is used to
+     * render an outbox message and they are enabled by the threema setting.
+     * This workaround is necessary because we actually use different color
+     * references when this returns true.
+     */
+    private boolean shouldUseDynamicColors() {
+        return isUsedForOutboxMessage && ColorUtil.areDynamicColorsCurrentlyApplied(getContext());
+    }
+
+    /**
+     *  We do this color workaround to render this view on the left side of the chat in its fixed colors (old way)
+     *  and on the right side with the <strong>possibility</strong> to show itself in dynamic colors.
+     */
+    private @ColorInt int getBackgroundDefaultColor() {
+        if (shouldUseDynamicColors()) {
+            return ConfigUtils.getColorFromAttribute(getContext(), R.attr.colorPrimary);
+        } else {
+            return getResources().getColor(
+                ColorUtil.shouldUseDarkVariant(getContext())
+                    ? R.color.md_theme_dark_tertiaryContainer
+                    : R.color.md_theme_light_tertiaryContainer
+            );
+        }
+    }
+
+    /**
+     *  We do this color workaround to render this view on the left side of the chat in its fixed colors (old way)
+     *  and on the right side with the <strong>possibility</strong> to show itself in dynamic colors.
+     */
+    private @ColorInt int getProgressTrackColor() {
+        if (shouldUseDynamicColors()) {
+            return ConfigUtils.getColorFromAttribute(getContext(), R.attr.colorSurfaceBright);
+        } else {
+            return getResources().getColor(
+                ColorUtil.shouldUseDarkVariant(getContext())
+                    ? R.color.md_theme_dark_primaryContainer
+                    : R.color.md_theme_light_primaryContainer
+            );
+        }
+    }
+
+    /**
+     *  We do this color workaround to render this view on the left side of the chat in its fixed colors (old way)
+     *  and on the right side with the <strong>possibility</strong> to show itself in dynamic colors.
+     */
+    private @ColorInt int getProgressTrackIndicatorColor() {
+        if (shouldUseDynamicColors()) {
+            return ConfigUtils.getColorFromAttribute(getContext(), R.attr.colorOnPrimary);
+        } else {
+            return getResources().getColor(
+                ColorUtil.shouldUseDarkVariant(getContext())
+                    ? R.color.md_theme_dark_primary
+                    : R.color.md_theme_light_primary
+            );
+        }
+    }
+
+    private @ColorInt int getIconTintColor() {
+        return ConfigUtils.getColorFromAttribute(
+            getContext(),
+            shouldUseDynamicColors()
+                ? R.attr.colorOnPrimary
+                : R.attr.colorOnBackground
+        );
+    }
 }

+ 6 - 1
app/src/main/java/ch/threema/app/ui/EmptyView.java

@@ -22,6 +22,7 @@
 package ch.threema.app.ui;
 
 import android.content.Context;
+import android.graphics.PorterDuff;
 import android.util.AttributeSet;
 import android.util.TypedValue;
 import android.view.Gravity;
@@ -36,6 +37,7 @@ import android.widget.TextView;
 import androidx.annotation.ColorInt;
 import androidx.annotation.ColorRes;
 import androidx.annotation.DrawableRes;
+import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
 
 import com.google.android.material.progressindicator.CircularProgressIndicator;
@@ -85,8 +87,11 @@ public class EmptyView extends LinearLayout {
 		this.emptyText.setText(label);
 	}
 
-	public void setup(@StringRes int labelRes, @DrawableRes int imageRes) {
+	public void setup(@StringRes int labelRes, @DrawableRes int imageRes, @Nullable @ColorInt Integer imageTint) {
 		this.emptyImageView.setImageResource(imageRes);
+        if (imageTint != null) {
+            this.emptyImageView.setColorFilter(imageTint, PorterDuff.Mode.SRC_IN);
+        }
 		this.emptyImageView.setVisibility(VISIBLE);
 		this.emptyText.setText(labelRes);
 	}

+ 1 - 1
app/src/main/java/ch/threema/app/ui/OpenBallotNoticeView.java

@@ -391,7 +391,7 @@ public class OpenBallotNoticeView extends ConstraintLayout implements DefaultLif
 			SpannableString s = new SpannableString(highlightItem.getTitle());
 			s.setSpan(new ForegroundColorSpan(highlightColor), 0, s.length(), 0);
 			highlightItem.setTitle(s);
-			ConfigUtils.tintMenuItem(highlightItem, highlightColor);
+			ConfigUtils.tintMenuIcon(highlightItem, highlightColor);
 
 			menuBuilder.setCallback(new MenuBuilder.Callback() {
 				@Override

+ 1 - 1
app/src/main/java/ch/threema/app/ui/listitemholder/ComposeMessageHolder.java

@@ -53,7 +53,7 @@ public class ComposeMessageHolder extends AvatarListItemHolder {
 	public TranscoderView transcoderView;
 	public FrameLayout readOnContainer;
 	public Chip readOnButton;
-	public View messageTypeButton;
+	public ImageView audioMessageIcon;
 	public View groupAckContainer;
 	public ImageView groupAckThumbsUpImage, groupAckThumbsDownImage;
 	public TextView groupAckThumbsUpCount, groupAckThumbsDownCount;

+ 121 - 99
app/src/main/java/ch/threema/app/utils/ColorUtil.java

@@ -22,108 +22,130 @@
 package ch.threema.app.utils;
 
 import android.content.Context;
+import android.content.res.Configuration;
 import android.graphics.Bitmap;
 import android.graphics.Color;
 
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import ch.threema.app.R;
+
 public class ColorUtil {
 
-	/* This is the default gray for the light theme. Note that it is darker than COLOR_GRAY_DARK to increase the contrasts. */
-	public final static int COLOR_GRAY_LIGHT = 0xFF777777;
-
-	/* This is the default gray for the dark theme. Note that it is lighter than COLOR_GRAY_LIGHT to increase the contrasts. */
-	public final static int COLOR_GRAY_DARK = 0xFFAAAAAA;
-
-	/* The light id colors */
-	private final static String[] ID_COLOR_LIGHT = new String[]{"#d84315", "#ef6c00", "#ff8f00", "#eb9c00", "#9e9d24", "#7cb342", "#2e7d32", "#00796b", "#0097a7", "#0288d1", "#1565c0", "#283593", "#7b3ab7", "#ac24aa", "#ad1457", "#c62828"};
-
-	/* The dark id colors */
-	private final static String[] ID_COLOR_DARK = new String[]{"#ff7043", "#ffa726", "#ffca28", "#fff176", "#a6a626", "#8bc34a", "#66bb6a", "#2ab7a9", "#26c6da", "#4fc3f7", "#42a5f5", "#8a93ff", "#a88ce3", "#c680d1", "#f16f9a", "#f2706e"};
-
-	// Singleton stuff
-	private static ColorUtil sInstance = null;
-
-	public static synchronized ColorUtil getInstance() {
-		if (sInstance == null) {
-			sInstance = new ColorUtil();
-		}
-		return sInstance;
-	}
-
-	/**
-	 * Get the light ID color at the given index. If the index is out of range, the default gray color is returned.
-	 *
-	 * @param index the index based on the hash
-	 * @return the ID color at the given index
-	 */
-	public int getIDColorLight(int index) {
-		if (index < 0 || index >= ID_COLOR_LIGHT.length) {
-			return COLOR_GRAY_LIGHT;
-		}
-		return Color.parseColor(ID_COLOR_LIGHT[index]);
-	}
-
-	/**
-	 * Get the dark ID color at the given index. If the index is out of range, the default gray color is returned.
-	 *
-	 * @param index the index based on the hash
-	 * @return the ID color at the given index
-	 */
-	public int getIDColorDark(int index) {
-		if (index < 0 || index >= ID_COLOR_DARK.length) {
-			return COLOR_GRAY_DARK;
-		}
-		return Color.parseColor(ID_COLOR_DARK[index]);
-	}
-
-	/**
-	 * Get the color index for the given first byte of the ID color hash.
-	 *
-	 * @param firstByte the first byte of the hash
-	 * @return the color for the first byte
-	 */
-	public int getIDColorIndex(byte firstByte) {
-		return (((int) firstByte) & 0xff) / ID_COLOR_LIGHT.length;
-	}
-
-	/**
-	 * Get the default gray based on the current theme. In light theme a darker gray is returned to
-	 * increase the contrasts.
-	 *
-	 * @param context the context is needed to determine the current app theme
-	 * @return the gray based on the theme
-	 */
-	public int getCurrentThemeGray(Context context) {
-		if (ConfigUtils.isTheDarkSide(context)) {
-			return COLOR_GRAY_DARK;
-		} else {
-			return COLOR_GRAY_LIGHT;
-		}
-	}
-
-	/*
-		Calculates the estimated brightness of an Android Bitmap.
-		pixelSpacing tells how many pixels to skip each pixel. Higher values result in better performance, but a more rough estimate.
-		When pixelSpacing = 1, the method actually calculates the real average brightness, not an estimate.
-		This is what the calculateBrightness() shorthand is for.
-		Do not use values for pixelSpacing that are smaller than 1.
-	*/
-	public int calculateBrightness(Bitmap bitmap, int pixelSpacing) {
-		int R = 0; int G = 0; int B = 0;
-		int height = bitmap.getHeight();
-		int width = bitmap.getWidth();
-		int n = 0;
-		int[] pixels = new int[width * height];
-		bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
-		for (int i = 0; i < pixels.length; i += pixelSpacing) {
-			int color = pixels[i];
-			R += Color.red(color);
-			G += Color.green(color);
-			B += Color.blue(color);
-			n++;
-		}
-		if (n != 0) {
-			return (R + B + G) / (n * 3);
-		}
-		return 0;
-	}
+    /* This is the default gray for the light theme. Note that it is darker than COLOR_GRAY_DARK to increase the contrasts. */
+    public final static int COLOR_GRAY_LIGHT = 0xFF777777;
+
+    /* This is the default gray for the dark theme. Note that it is lighter than COLOR_GRAY_LIGHT to increase the contrasts. */
+    public final static int COLOR_GRAY_DARK = 0xFFAAAAAA;
+
+    /* The light id colors */
+    private final static String[] ID_COLOR_LIGHT = new String[]{"#d84315", "#ef6c00", "#ff8f00", "#eb9c00", "#9e9d24", "#7cb342", "#2e7d32", "#00796b", "#0097a7", "#0288d1", "#1565c0", "#283593", "#7b3ab7", "#ac24aa", "#ad1457", "#c62828"};
+
+    /* The dark id colors */
+    private final static String[] ID_COLOR_DARK = new String[]{"#ff7043", "#ffa726", "#ffca28", "#fff176", "#a6a626", "#8bc34a", "#66bb6a", "#2ab7a9", "#26c6da", "#4fc3f7", "#42a5f5", "#8a93ff", "#a88ce3", "#c680d1", "#f16f9a", "#f2706e"};
+
+    // Singleton stuff
+    private static ColorUtil sInstance = null;
+
+    public static synchronized ColorUtil getInstance() {
+        if (sInstance == null) {
+            sInstance = new ColorUtil();
+        }
+        return sInstance;
+    }
+
+    /**
+     * Get the light ID color at the given index. If the index is out of range, the default gray color is returned.
+     *
+     * @param index the index based on the hash
+     * @return the ID color at the given index
+     */
+    public int getIDColorLight(int index) {
+        if (index < 0 || index >= ID_COLOR_LIGHT.length) {
+            return COLOR_GRAY_LIGHT;
+        }
+        return Color.parseColor(ID_COLOR_LIGHT[index]);
+    }
+
+    /**
+     * Get the dark ID color at the given index. If the index is out of range, the default gray color is returned.
+     *
+     * @param index the index based on the hash
+     * @return the ID color at the given index
+     */
+    public int getIDColorDark(int index) {
+        if (index < 0 || index >= ID_COLOR_DARK.length) {
+            return COLOR_GRAY_DARK;
+        }
+        return Color.parseColor(ID_COLOR_DARK[index]);
+    }
+
+    /**
+     * Get the color index for the given first byte of the ID color hash.
+     *
+     * @param firstByte the first byte of the hash
+     * @return the color for the first byte
+     */
+    public int getIDColorIndex(byte firstByte) {
+        return (((int) firstByte) & 0xff) / ID_COLOR_LIGHT.length;
+    }
+
+    /**
+     * Get the default gray based on the current theme. In light theme a darker gray is returned to
+     * increase the contrasts.
+     *
+     * @param context the context is needed to determine the current app theme
+     * @return the gray based on the theme
+     */
+    public int getCurrentThemeGray(Context context) {
+        if (ConfigUtils.isTheDarkSide(context)) {
+            return COLOR_GRAY_DARK;
+        } else {
+            return COLOR_GRAY_LIGHT;
+        }
+    }
+
+    /*
+        Calculates the estimated brightness of an Android Bitmap.
+        pixelSpacing tells how many pixels to skip each pixel. Higher values result in better performance, but a more rough estimate.
+        When pixelSpacing = 1, the method actually calculates the real average brightness, not an estimate.
+        This is what the calculateBrightness() shorthand is for.
+        Do not use values for pixelSpacing that are smaller than 1.
+    */
+    public int calculateBrightness(Bitmap bitmap, int pixelSpacing) {
+        int R = 0;
+        int G = 0;
+        int B = 0;
+        int height = bitmap.getHeight();
+        int width = bitmap.getWidth();
+        int n = 0;
+        int[] pixels = new int[width * height];
+        bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
+        for (int i = 0; i < pixels.length; i += pixelSpacing) {
+            int color = pixels[i];
+            R += Color.red(color);
+            G += Color.green(color);
+            B += Color.blue(color);
+            n++;
+        }
+        if (n != 0) {
+            return (R + B + G) / (n * 3);
+        }
+        return 0;
+    }
+
+    public static boolean shouldUseDarkVariant(final @NonNull Context context) {
+        int nightModeFlags = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
+        return nightModeFlags == Configuration.UI_MODE_NIGHT_YES;
+    }
+
+    public static boolean areDynamicColorsCurrentlyApplied(final @NonNull Context context) {
+        final @ColorInt int staticColorPrimary = context.getResources().getColor(
+            shouldUseDarkVariant(context)
+                ? R.color.md_theme_dark_primary
+                : R.color.md_theme_light_primary
+        );
+        final @ColorInt int dynamicColorPrimary = ConfigUtils.getColorFromAttribute(context, R.attr.colorPrimary);
+        return staticColorPrimary != dynamicColorPrimary;
+    }
 }

+ 9 - 20
app/src/main/java/ch/threema/app/utils/ConfigUtils.java

@@ -57,7 +57,6 @@ import android.util.DisplayMetrics;
 import android.util.TypedValue;
 import android.view.Menu;
 import android.view.MenuItem;
-import android.view.SubMenu;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowManager;
@@ -480,20 +479,17 @@ public class ConfigUtils {
      * @param menu  The menu to tint
      * @param color The ColorInt to tint the icons
      */
-    public static void tintMenu(Menu menu, @ColorInt int color) {
+    public static void tintMenuIcons(@NonNull Menu menu, @ColorInt int color) {
         for (int i = 0, size = menu.size(); i < size; i++) {
             final MenuItem menuItem = menu.getItem(i);
-            tintMenuItem(menuItem, color);
-            if (menuItem.hasSubMenu()) {
-                final SubMenu subMenu = menuItem.getSubMenu();
-                for (int j = 0; j < subMenu.size(); j++) {
-                    tintMenuItem(subMenu.getItem(j), color);
-                }
+            tintMenuIcon(menuItem, color);
+            if (menuItem.hasSubMenu() && menuItem.getSubMenu() != null) {
+                tintMenuIcons(menuItem.getSubMenu(), color);
             }
         }
     }
 
-    public static void tintMenuItem(@Nullable final MenuItem menuItem, @ColorInt int color) {
+    public static void tintMenuIcon(@Nullable final MenuItem menuItem, @ColorInt int color) {
         if (menuItem != null) {
             final Drawable drawable = menuItem.getIcon();
             if (drawable != null) {
@@ -503,8 +499,8 @@ public class ConfigUtils {
         }
     }
 
-    public static void tintMenuItem(@NonNull Context context, @Nullable final MenuItem menuItem, @AttrRes int colorAttr) {
-        tintMenuItem(menuItem, getColorFromAttribute(context, colorAttr));
+    public static void tintMenuIcon(@NonNull Context context, @Nullable final MenuItem menuItem, @AttrRes int colorAttr) {
+        tintMenuIcon(menuItem, getColorFromAttribute(context, colorAttr));
     }
 
     public static void setEmojiStyle(Context context, int newStyle) {
@@ -1467,13 +1463,10 @@ public class ConfigUtils {
     }
 
     /**
-     * Configure menu to display icons and dividers. Call this in onCreateOptionsMenu()
-     *
-     * @param context Context - required for theming, set to null if you want the icon color not to be touched
-     * @param menu    Menu to configure
+     * Configure menu to display dividers. Call this in onCreateOptionsMenu()
      */
     @SuppressLint("RestrictedApi")
-    public static void addIconsToOverflowMenu(@Nullable Context context, @NonNull Menu menu) {
+    public static void addIconsToOverflowMenu(@NonNull Menu menu) {
         MenuCompat.setGroupDividerEnabled(menu, true);
 
         try {
@@ -1481,10 +1474,6 @@ public class ConfigUtils {
             if (menu instanceof MenuBuilder) {
                 MenuBuilder menuBuilder = (MenuBuilder) menu;
                 menuBuilder.setOptionalIconsVisible(true);
-
-                if (context != null) {
-                    ConfigUtils.tintMenu(menu, ConfigUtils.getColorFromAttribute(context, R.attr.colorOnSurface));
-                }
             }
         } catch (Exception ignored) {
         }

+ 3 - 3
app/src/main/java/ch/threema/app/utils/ContactUtil.java

@@ -120,10 +120,10 @@ public class ContactUtil {
 			|| ThreemaApplication.ECHO_USER_IDENTITY.equals(identity);
 	}
 
-	public static boolean canReceiveVoipMessages(ContactModel contactModel, IdListService blackListIdentityService) {
+	public static boolean canReceiveVoipMessages(ContactModel contactModel, IdListService blockedContactsService) {
 		return contactModel != null
-				&& blackListIdentityService != null
-				&& !blackListIdentityService.has(contactModel.getIdentity())
+				&& blockedContactsService != null
+				&& !blockedContactsService.has(contactModel.getIdentity())
 				&& !isEchoEchoOrGatewayContact(contactModel);
 	}
 

+ 10 - 10
app/src/main/java/ch/threema/app/utils/OutgoingCspMessageUtils.kt

@@ -102,8 +102,8 @@ fun Collection<ContactModel>.filterValid() = filter { it.state != ContactModel.S
 /**
  * Only include non-blocked contacts. Note that only explicitly blocked identities are excluded.
  */
-fun Collection<ContactModel>.filterNotBlocked(blackListService: IdListService) =
-    filterNotBlockedIf(true, blackListService)
+fun Collection<ContactModel>.filterNotBlocked(blockedContactsService: IdListService) =
+    filterNotBlockedIf(true, blockedContactsService)
 
 /**
  * Only include non-blocked contacts if [applyFilter] is true. Note that only explicitly blocked
@@ -111,8 +111,8 @@ fun Collection<ContactModel>.filterNotBlocked(blackListService: IdListService) =
  */
 fun Collection<ContactModel>.filterNotBlockedIf(
     applyFilter: Boolean,
-    blackListService: IdListService,
-) = filterIf(applyFilter) { !blackListService.has(it.identity) }
+    blockedContactsService: IdListService,
+) = filterIf(applyFilter) { !blockedContactsService.has(it.identity) }
 
 /**
  * Filter the broadcast identity if no messages should be sent to it according to
@@ -300,7 +300,7 @@ suspend fun ActiveTaskCodec.sendContactMessage(
     identityStore: IdentityStoreInterface,
     contactStore: ContactStore,
     nonceFactory: NonceFactory,
-    blackListService: IdListService,
+    blockedContactsService: IdListService,
     taskCreator: TaskCreator,
 ): OutgoingMessageResult? = sendMessageToReceivers(
     messageCreator,
@@ -309,7 +309,7 @@ suspend fun ActiveTaskCodec.sendContactMessage(
     identityStore,
     contactStore,
     nonceFactory,
-    blackListService,
+    blockedContactsService,
     taskCreator
 ).firstOrNull()
 
@@ -332,7 +332,7 @@ suspend fun ActiveTaskCodec.sendGroupMessage(
     contactStore: ContactStore,
     nonceFactory: NonceFactory,
     groupService: GroupService,
-    blackListService: IdListService,
+    blockedContactsService: IdListService,
     taskCreator: TaskCreator,
 ): Set<OutgoingMessageResult>? {
     if (!groupService.isGroupMember(group)) {
@@ -349,7 +349,7 @@ suspend fun ActiveTaskCodec.sendGroupMessage(
         identityStore,
         contactStore,
         nonceFactory,
-        blackListService,
+        blockedContactsService,
         taskCreator
     )
 }
@@ -367,7 +367,7 @@ suspend fun ActiveTaskCodec.sendMessageToReceivers(
     identityStore: IdentityStoreInterface,
     contactStore: ContactStore,
     nonceFactory: NonceFactory,
-    blackListService: IdListService,
+    blockedContactsService: IdListService,
     taskCreator: TaskCreator,
 ): Set<OutgoingMessageResult> {
     val myIdentity = identityStore.identity
@@ -394,7 +394,7 @@ suspend fun ActiveTaskCodec.sendMessageToReceivers(
     // Create list of recipients that are not blocked (or exempted from blocking) with the messages
     val recipientMessageList = recipientList.zip(messageList).filter { (recipient, message) ->
         //Filter blocked recipients except message type is exempted from blocking
-        (message.exemptFromBlocking() || !blackListService.has(recipient.identity)).also {
+        (message.exemptFromBlocking() || !blockedContactsService.has(recipient.identity)).also {
             if (!it) {
                 message.logMessage("Skipping message because recipient $recipient is blocked:")
             }

+ 21 - 4
app/src/main/java/ch/threema/app/utils/StateBitmapUtil.java

@@ -28,11 +28,11 @@ import android.widget.ImageView;
 import java.util.EnumMap;
 import java.util.Map;
 
+import androidx.annotation.ColorInt;
 import androidx.annotation.DrawableRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
-import androidx.core.content.ContextCompat;
 import ch.threema.app.R;
 import ch.threema.app.ui.listitemholder.ComposeMessageHolder;
 import ch.threema.storage.models.AbstractMessageModel;
@@ -109,7 +109,19 @@ public class StateBitmapUtil {
             : null;
     }
 
-    public void setStateDrawable(Context context, AbstractMessageModel messageModel, @Nullable ImageView imageView, boolean useInverseColors) {
+    /**
+     * Sets the icon depending on the messages state property into the passed {@code imageView}. When drawing the icon on a chat bubble view,
+     * the tint should already be correct. Otherwise use the {@code tintOverride} to set a custom color.
+     *
+     * @param tintOverride If passed, it will be applied to any message state icon where the state is not either {@code SENDFAILED}, {@code FS_KEY_MISMATCH},
+     * {@code USERACK} or {@code USERDEC}.
+     */
+    public void setStateDrawable(
+        Context context,
+        AbstractMessageModel messageModel,
+        @Nullable ImageView imageView,
+        @Nullable @ColorInt Integer tintOverride
+    ) {
         if (imageView == null) {
             return;
         }
@@ -141,8 +153,13 @@ public class StateBitmapUtil {
                     imageView.setImageTintList(null);
                     imageView.setColorFilter(this.decColor);
                 } else {
-                    imageView.setColorFilter(null);
-                    imageView.setImageTintList(ContextCompat.getColorStateList(context, R.color.bubble_text_colorstatelist));
+                    if (tintOverride == null) {
+                        imageView.setColorFilter(null);
+                        imageView.setImageTintList(messageModel.getUiContentColor(context));
+                    } else {
+                        imageView.setImageTintList(null);
+                        imageView.setColorFilter(tintOverride);
+                    }
                 }
             }
         }

+ 11 - 8
app/src/main/java/ch/threema/app/voip/services/VoipStateService.java

@@ -732,16 +732,21 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 
         final ContactMessageReceiver messageReceiver = this.contactService.createReceiver(contact);
 
-        boolean isMuted = DNDUtil.getInstance().isMutedPrivate(messageReceiver, null);
-
         // Set state to RINGING
         this.setStateRinging(callId);
 
         // Show call notification
         final Notification notification = this.showNotification(contact, accept, reject, msg);
 
+        DNDUtil dndUtil = DNDUtil.getInstance();
+        boolean isReceiverMuted = dndUtil.isMutedPrivate(messageReceiver, null);
+        boolean isSystemMuted = dndUtil.isSystemMuted(
+            messageReceiver, notification, notificationManagerCompat
+        );
+        boolean isMuted = isReceiverMuted || isSystemMuted;
+
         // Play ringtone
-        this.playRingtone(notification, messageReceiver, isMuted);
+        this.playRingtone(messageReceiver, isMuted);
 
         // Vibrate if necessary
         this.startVibration(notification, isMuted);
@@ -1628,7 +1633,7 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
         return notification;
     }
 
-    private void playRingtone(@Nullable Notification notification, MessageReceiver messageReceiver, boolean isMuted) {
+    private void playRingtone(MessageReceiver<?> messageReceiver, boolean isMuted) {
         final Uri ringtoneUri = this.ringtoneService.getVoiceCallRingtone(messageReceiver.getUniqueIdString());
 
         if (ringtoneUri != null) {
@@ -1637,9 +1642,7 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
                 stopRingtone();
             }
 
-            boolean isSystemMuted = DNDUtil.getInstance().isSystemMuted(messageReceiver, notification, notificationManagerCompat);
-
-            if (!isMuted && !isSystemMuted) {
+            if (!isMuted) {
                 audioManager.requestAudioFocus(this, AudioManager.STREAM_RING, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
                 ringtonePlayer = new MediaPlayerStateWrapper();
                 ringtonePlayer.setStateListener(new MediaPlayerStateWrapper.StateListener() {
@@ -1674,7 +1677,7 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
                     stopRingtone();
                 }
             } else {
-                logger.info("Not playing ringtone. isMuted = {}, isSystemMuted = {}", isMuted, isSystemMuted);
+                logger.info("Not playing ringtone. isMuted = {}", isMuted);
             }
         } else {
             logger.info("No ringtone selected");

+ 1 - 1
app/src/main/java/ch/threema/app/voip/util/VoipUtil.java

@@ -137,7 +137,7 @@ public class VoipUtil {
 			return false;
 		}
 
-		if (serviceManager.getBlackListService().has(contactModel.getIdentity())) {
+		if (serviceManager.getBlockedContactsService().has(contactModel.getIdentity())) {
 			Toast.makeText(activity, R.string.blocked_cannot_send, Toast.LENGTH_LONG).show();
 			return false;
 		}

+ 1 - 1
app/src/main/java/ch/threema/app/webclient/activities/SessionsActivity.java

@@ -943,7 +943,7 @@ public class SessionsActivity extends ThreemaToolbarActivity
         super.onCreateOptionsMenu(menu);
         this.getMenuInflater().inflate(R.menu.activity_webclient_sessions, menu);
 
-        ConfigUtils.addIconsToOverflowMenu(this, menu);
+        ConfigUtils.addIconsToOverflowMenu(menu);
 
         return true;
     }

+ 1 - 1
app/src/main/java/ch/threema/app/webclient/converter/Contact.java

@@ -96,7 +96,7 @@ public class Contact extends Converter {
 			builder.maybePut(IS_WORK, ConfigUtils.isWorkBuild() && contact.isWork());
 			builder.put(PUBLIC_KEY, contact.getPublicKey());
 			builder.put(IDENTITY_TYPE, contact.getIdentityType() == IdentityType.WORK ? 1 : 0);
-			builder.put(IS_BLOCKED, getBlackListService().has(contact.getIdentity()));
+			builder.put(IS_BLOCKED, getBlockedContactsService().has(contact.getIdentity()));
 
 			final long featureMask = contact.getFeatureMask();
 			builder.put(FEATURE_MASK, featureMask);

+ 2 - 2
app/src/main/java/ch/threema/app/webclient/converter/Converter.java

@@ -56,8 +56,8 @@ public abstract class Converter {
 		return serviceManager;
 	}
 
-	protected static IdListService getBlackListService() {
-		return getServiceManager().getBlackListService();
+	protected static IdListService getBlockedContactsService() {
+		return getServiceManager().getBlockedContactsService();
 	}
 
 	protected static ContactService getContactService() throws ConversionException {

+ 3 - 3
app/src/main/java/ch/threema/app/webclient/services/ServicesContainer.java

@@ -61,7 +61,7 @@ public class ServicesContainer {
 	@NonNull public final MessageService message;
 	@NonNull public final NotificationService notification;
 	@NonNull public final DatabaseServiceNew database;
-	@NonNull public final IdListService blackList;
+	@NonNull public final IdListService blockedContactsService;
 	@NonNull public final PreferenceService preference;
 	@NonNull public final UserService user;
 	@NonNull public final DeadlineListService hiddenChat;
@@ -83,7 +83,7 @@ public class ServicesContainer {
 		@NonNull final MessageService message,
 		@NonNull final NotificationService notification,
 		@NonNull final DatabaseServiceNew database,
-		@NonNull final IdListService blackList,
+		@NonNull final IdListService blockedContactsService,
 		@NonNull final PreferenceService preference,
 		@NonNull final UserService user,
 		@NonNull final DeadlineListService hiddenChat,
@@ -101,7 +101,7 @@ public class ServicesContainer {
 		this.message = message;
 		this.notification = notification;
 		this.database = database;
-		this.blackList = blackList;
+		this.blockedContactsService = blockedContactsService;
 		this.preference = preference;
 		this.user = user;
 		this.hiddenChat = hiddenChat;

+ 2 - 2
app/src/main/java/ch/threema/app/webclient/services/instance/SessionInstanceServiceImpl.java

@@ -428,13 +428,13 @@ public class SessionInstanceServiceImpl implements SessionInstanceService {
 			createDispatcher,
 			services.message,
 			services.lifetime,
-			services.blackList));
+			services.blockedContactsService));
 		createDispatcher.addReceiver(new FileMessageCreateHandler(
 			createDispatcher,
 			services.message,
 			services.file,
 			services.lifetime,
-			services.blackList));
+			services.blockedContactsService));
 
 		createDispatcher.addReceiver(new CreateContactHandler(
 			createDispatcher,

+ 2 - 2
app/src/main/java/ch/threema/app/webclient/services/instance/message/receiver/FileMessageCreateHandler.java

@@ -89,8 +89,8 @@ public class FileMessageCreateHandler extends MessageCreateHandler {
 	                                MessageService messageService,
 	                                FileService fileService,
 	                                LifetimeService lifetimeService,
-	                                IdListService blackListService) {
-		super(Protocol.SUB_TYPE_FILE_MESSAGE, dispatcher, messageService, lifetimeService, blackListService);
+	                                IdListService blockedContactsService) {
+		super(Protocol.SUB_TYPE_FILE_MESSAGE, dispatcher, messageService, lifetimeService, blockedContactsService);
 
 		this.fileService = fileService;
 	}

+ 4 - 4
app/src/main/java/ch/threema/app/webclient/services/instance/message/receiver/MessageCreateHandler.java

@@ -57,7 +57,7 @@ abstract public class MessageCreateHandler extends MessageReceiver {
 	private final MessageDispatcher dispatcher;
 	protected final MessageService messageService;
 	private final LifetimeService lifetimeService;
-	private final IdListService blacklistService;
+	private final IdListService blockedContactsService;
 
 	/**
 	 * A validation error.
@@ -80,13 +80,13 @@ abstract public class MessageCreateHandler extends MessageReceiver {
 		MessageDispatcher dispatcher,
 		MessageService messageService,
 		LifetimeService lifetimeService,
-		IdListService blacklistService
+		IdListService blockedContactsService
 	) {
 		super(subType);
 		this.dispatcher = dispatcher;
 		this.messageService = messageService;
 		this.lifetimeService = lifetimeService;
-		this.blacklistService = blacklistService;
+		this.blockedContactsService = blockedContactsService;
 
 	}
 
@@ -116,7 +116,7 @@ abstract public class MessageCreateHandler extends MessageReceiver {
 			ch.threema.app.messagereceiver.MessageReceiver receiver = receiverModel.getReceiver();
 			if (receiver.getType() == receiver.Type_CONTACT) {
 				ContactModel receiverContact = ((ContactMessageReceiver) receiver).getContact();
-				if (receiverContact != null && this.blacklistService.has(receiverContact.getIdentity())) {
+				if (receiverContact != null && this.blockedContactsService.has(receiverContact.getIdentity())) {
 					throw new MessageCreateHandler.MessageValidationException("blocked", false);
 				}
 			}

+ 2 - 2
app/src/main/java/ch/threema/app/webclient/services/instance/message/receiver/TextMessageCreateHandler.java

@@ -63,8 +63,8 @@ public class TextMessageCreateHandler extends MessageCreateHandler {
 	public TextMessageCreateHandler(MessageDispatcher dispatcher,
 	                                MessageService messageService,
 	                                LifetimeService lifetimeService,
-	                                IdListService blackListService) {
-		super(Protocol.SUB_TYPE_TEXT_MESSAGE, dispatcher, messageService, lifetimeService, blackListService);
+	                                IdListService blockedContactsService) {
+		super(Protocol.SUB_TYPE_TEXT_MESSAGE, dispatcher, messageService, lifetimeService, blockedContactsService);
 	}
 
 	@Override

+ 22 - 0
app/src/main/java/ch/threema/storage/models/AbstractMessageModel.java

@@ -22,11 +22,17 @@
 package ch.threema.storage.models;
 
 
+import android.content.Context;
+import android.content.res.ColorStateList;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import java.util.Date;
 
+import androidx.appcompat.content.res.AppCompatResources;
+import ch.threema.app.R;
+import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.QuoteUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.domain.protocol.csp.messages.fs.ForwardSecurityMode;
@@ -649,6 +655,22 @@ public abstract class AbstractMessageModel {
         return (getDisplayTags() & DisplayTag.DISPLAY_TAG_STARRED) == DisplayTag.DISPLAY_TAG_STARRED;
     }
 
+    /**
+     * @return The correct color-state-list to use when rendering contents of this message model.
+     */
+    @NonNull
+    public ColorStateList getUiContentColor(@NonNull Context context) {
+        if (isStatusMessage || this instanceof FirstUnreadMessageModel) {
+            return ColorStateList.valueOf(
+                ConfigUtils.getColorFromAttribute(context, R.attr.colorOnTertiaryContainer)
+            );
+        }
+        return AppCompatResources.getColorStateList(
+            context,
+            isOutbox() ? R.color.bubble_send_text_colorstatelist : R.color.bubble_receive_text_colorstatelist
+        );
+    }
+
     /**
      * TODO(ANDR-XXXX): evil code!
      *

+ 1 - 1
app/src/main/java/ch/threema/storage/models/ContactModel.java

@@ -572,7 +572,7 @@ public class ContactModel extends Contact implements ReceiverModel {
         if (isWork && jobTitle != null && !jobTitle.isBlank()) {
             return jobTitle.trim();
         } else if (publicNickName != null && !publicNickName.isBlank()) {
-            return publicNickName.trim();
+            return TILDE + publicNickName.trim();
         } else {
             return "";
         }

+ 0 - 0
app/src/main/res/color/bubble_text_colorstatelist.xml → app/src/main/res/color/bubble_receive_text_colorstatelist.xml


+ 5 - 0
app/src/main/res/color/bubble_send_text_colorstatelist.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+	<item android:state_activated="true" android:color="?attr/colorOnPrimary"/>
+	<item android:color="?attr/colorOnSecondaryContainer"/>
+</selector>

+ 7 - 7
app/src/main/res/drawable/ic_circle_send.xml

@@ -1,10 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?>
 <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
-	<item>
-		<shape
-		       android:shape="oval"
-		       android:useLevel="false">
-			<solid android:color="?attr/colorPrimaryContainer"/>
-		</shape>
-	</item>
+    <item>
+        <shape
+            android:shape="oval"
+            android:useLevel="false">
+            <solid android:color="?attr/colorPrimaryContainer" />
+        </shape>
+    </item>
 </layer-list>

+ 7 - 7
app/src/main/res/drawable/ic_circle_send_disabled.xml

@@ -1,10 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?>
 <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
-	<item>
-		<shape
-		       android:shape="oval"
-		       android:useLevel="false">
-			<solid android:color="?attr/colorSurfaceVariant"/>
-		</shape>
-	</item>
+    <item>
+        <shape
+            android:shape="oval"
+            android:useLevel="false">
+            <solid android:color="?attr/colorSurfaceVariant" />
+        </shape>
+    </item>
 </layer-list>

+ 1 - 1
app/src/main/res/drawable/ic_collections.xml

@@ -5,4 +5,4 @@
     android:viewportWidth="24"
     android:viewportHeight="24">
     <path android:fillColor="#000" android:pathData="M21,17H7V3H21M21,1H7A2,2 0 0,0 5,3V17A2,2 0 0,0 7,19H21A2,2 0 0,0 23,17V3A2,2 0 0,0 21,1M3,5H1V21A2,2 0 0,0 3,23H19V21H3M15.96,10.29L13.21,13.83L11.25,11.47L8.5,15H19.5L15.96,10.29Z" />
-</vector>
+</vector>

+ 0 - 9
app/src/main/res/drawable/ic_places_hearing_aids.xml

@@ -1,9 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-	android:width="24dp"
-	android:height="24dp"
-	android:viewportHeight="24"
-	android:viewportWidth="24">
-	<path
-		android:fillColor="#000"
-		android:pathData="M17,20c-0.29,0 -0.56,-0.06 -0.76,-0.15 -0.71,-0.37 -1.21,-0.88 -1.71,-2.38 -0.51,-1.56 -1.47,-2.29 -2.39,-3 -0.79,-0.61 -1.61,-1.24 -2.32,-2.53C9.29,10.98 9,9.93 9,9c0,-2.8 2.2,-5 5,-5s5,2.2 5,5h2c0,-3.93 -3.07,-7 -7,-7S7,5.07 7,9c0,1.26 0.38,2.65 1.07,3.9 0.91,1.65 1.98,2.48 2.85,3.15 0.81,0.62 1.39,1.07 1.71,2.05 0.6,1.82 1.37,2.84 2.73,3.55 0.51,0.23 1.07,0.35 1.64,0.35 2.21,0 4,-1.79 4,-4h-2c0,1.1 -0.9,2 -2,2zM7.64,2.64L6.22,1.22C4.23,3.21 3,5.96 3,9s1.23,5.79 3.22,7.78l1.41,-1.41C6.01,13.74 5,11.49 5,9s1.01,-4.74 2.64,-6.36zM11.5,9c0,1.38 1.12,2.5 2.5,2.5s2.5,-1.12 2.5,-2.5 -1.12,-2.5 -2.5,-2.5 -2.5,1.12 -2.5,2.5z" />
-</vector>

+ 5 - 5
app/src/main/res/drawable/ic_search_outline.xml

@@ -1,9 +1,9 @@
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
     android:width="24dp"
     android:height="24dp"
-    android:viewportWidth="24"
-    android:viewportHeight="24">
-  <path
-      android:fillColor="#FF000000"
-      android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
 </vector>

+ 76 - 80
app/src/main/res/layout/activity_contact_detail.xml

@@ -1,51 +1,51 @@
 <?xml version="1.0" encoding="utf-8"?>
 <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
-												 xmlns:app="http://schemas.android.com/apk/res-auto"
-												 android:id="@+id/main_content"
-												 android:layout_width="match_parent"
-												 android:layout_height="match_parent"
-												 android:fitsSystemWindows="true"
-												android:background="?attr/colorSurface">
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/main_content"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="?attr/colorSurface"
+    android:fitsSystemWindows="true">
 
-	<com.google.android.material.appbar.AppBarLayout
-			android:id="@+id/appbar"
-			android:layout_width="match_parent"
-			android:layout_height="wrap_content"
-			android:theme="@style/ThemeOverlay.Material3.Dark.ActionBar"
-			android:importantForAccessibility="no"
-			android:fitsSystemWindows="true">
+    <com.google.android.material.appbar.AppBarLayout
+        android:id="@+id/appbar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:fitsSystemWindows="true"
+        android:importantForAccessibility="no"
+        android:theme="@style/ThemeOverlay.Material3.Dark.ActionBar">
 
-		<com.google.android.material.appbar.CollapsingToolbarLayout
-				style="?attr/collapsingToolbarLayoutLargeStyle"
-				android:id="@+id/collapsing_toolbar"
-				android:layout_width="match_parent"
-				android:layout_height="@dimen/contact_detail_avatar_height"
-				app:layout_scrollFlags="scroll|exitUntilCollapsed"
-				android:fitsSystemWindows="true"
-				android:importantForAccessibility="no"
-				app:contentScrim="?android:attr/colorBackground">
+        <com.google.android.material.appbar.CollapsingToolbarLayout
+            android:id="@+id/collapsing_toolbar"
+            style="?attr/collapsingToolbarLayoutLargeStyle"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/contact_detail_avatar_height"
+            android:fitsSystemWindows="true"
+            android:importantForAccessibility="no"
+            app:contentScrim="?android:attr/colorBackground"
+            app:layout_scrollFlags="scroll|exitUntilCollapsed">
 
             <!-- Large profile picture in the background -->
-			<ch.threema.app.ui.AvatarEditView
-				android:id="@+id/avatar_edit_view"
-				android:layout_width="match_parent"
-				android:layout_height="match_parent"
-				android:fitsSystemWindows="true"
-				android:importantForAccessibility="no"
-				android:contentDescription="@string/profile_picture"
-				app:layout_collapseMode="parallax"/>
+            <ch.threema.app.ui.AvatarEditView
+                android:id="@+id/avatar_edit_view"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:contentDescription="@string/profile_picture"
+                android:fitsSystemWindows="true"
+                android:importantForAccessibility="no"
+                app:layout_collapseMode="parallax" />
 
             <!-- Nickname and - potentially - the Threema Work icon. -->
-			<LinearLayout
-				android:layout_width="match_parent"
-				android:layout_height="wrap_content"
-				android:layout_marginLeft="16dp"
-				android:layout_marginRight="16dp"
-				android:layout_marginBottom="32dp"
-				android:layout_gravity="bottom|left"
-				android:gravity="bottom"
-				android:importantForAccessibility="no"
-				android:orientation="horizontal">
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="bottom|left"
+                android:layout_marginLeft="16dp"
+                android:layout_marginRight="16dp"
+                android:layout_marginBottom="32dp"
+                android:gravity="bottom"
+                android:importantForAccessibility="no"
+                android:orientation="horizontal">
 
                 <ch.threema.app.ui.HintedImageView
                     android:id="@+id/work_icon"
@@ -53,63 +53,59 @@
                     android:layout_height="30dp"
                     android:layout_marginRight="6dp"
                     android:layout_marginBottom="2dp"
-                    app:srcCompat="@drawable/ic_badge_work"
-                    android:foreground="@drawable/selector_avatar"
                     android:contentDescription="@string/threema_work_contact"
+                    android:foreground="@drawable/selector_avatar"
                     android:visibility="gone"
-                    />
+                    app:srcCompat="@drawable/ic_badge_work" />
 
                 <ch.threema.app.emojis.EmojiTextView
                     android:id="@+id/contact_title"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
-                    android:maxLines="2"
                     android:ellipsize="end"
+                    android:maxLines="2"
                     android:textAppearance="@style/Threema.TextAppearance.DetailTitle"
                     app:layout_anchor="@id/appbar"
-                    app:layout_anchorGravity="bottom|left"
-                    />
-			</LinearLayout>
+                    app:layout_anchorGravity="bottom|left" />
+            </LinearLayout>
 
-			<View
-				android:layout_width="match_parent"
-				android:layout_height="16dp"
-				android:layout_gravity="bottom"
-				android:background="@drawable/shape_detail"/>
+            <View
+                android:layout_width="match_parent"
+                android:layout_height="16dp"
+                android:layout_gravity="bottom"
+                android:background="@drawable/shape_detail" />
 
             <!-- Toolbar at the top -->
-			<com.google.android.material.appbar.MaterialToolbar
-					android:id="@+id/toolbar"
-					android:layout_width="match_parent"
-					android:layout_height="?attr/actionBarSize"
-					android:elevation="0dp"
-					app:popupTheme="@style/Threema.PopupTheme.TransparentStatusbar"
-					app:layout_collapseMode="pin" />
+            <com.google.android.material.appbar.MaterialToolbar
+                android:id="@+id/toolbar"
+                android:layout_width="match_parent"
+                android:layout_height="?attr/actionBarSize"
+                android:elevation="0dp"
+                app:layout_collapseMode="pin" />
 
-		</com.google.android.material.appbar.CollapsingToolbarLayout>
+        </com.google.android.material.appbar.CollapsingToolbarLayout>
 
-	</com.google.android.material.appbar.AppBarLayout>
+    </com.google.android.material.appbar.AppBarLayout>
 
-	<androidx.recyclerview.widget.RecyclerView
-			android:id="@+id/contact_group_list"
-			android:layout_width="match_parent"
-			android:layout_height="match_parent"
-			android:importantForAccessibility="no"
-			app:layout_behavior="@string/appbar_scrolling_view_behavior">
-	</androidx.recyclerview.widget.RecyclerView>
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/contact_group_list"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:importantForAccessibility="no"
+        app:layout_behavior="@string/appbar_scrolling_view_behavior" />
 
     <!-- FAB for editing contact -->
-	<com.google.android.material.floatingactionbutton.FloatingActionButton
-		android:id="@+id/floating"
-		android:layout_height="wrap_content"
-		android:layout_width="wrap_content"
-		app:fabSize="normal"
-		app:layout_anchor="@id/appbar"
-		app:layout_anchorGravity="bottom|right|end"
-		app:srcCompat="@drawable/ic_pencil_outline"
-		android:contentDescription="@string/edit_name_only"
-		android:layout_marginRight="@dimen/tablet_standard_padding_left_right"
-		android:layout_marginBottom="-40dp"
-		app:layout_insetEdge="bottom" />
+    <com.google.android.material.floatingactionbutton.FloatingActionButton
+        android:id="@+id/floating"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginRight="@dimen/tablet_standard_padding_left_right"
+        android:layout_marginBottom="-40dp"
+        android:contentDescription="@string/edit_name_only"
+        app:fabSize="normal"
+        app:layout_anchor="@id/appbar"
+        app:layout_anchorGravity="bottom|right|end"
+        app:layout_insetEdge="bottom"
+        app:srcCompat="@drawable/ic_pencil_outline" />
 
 </androidx.coordinatorlayout.widget.CoordinatorLayout>

+ 0 - 1
app/src/main/res/layout/activity_group_detail.xml

@@ -60,7 +60,6 @@
 					android:id="@+id/toolbar"
 					android:layout_width="match_parent"
 					android:layout_height="?attr/actionBarSize"
-					app:popupTheme="@style/Threema.PopupTheme.TransparentStatusbar"
 					app:layout_collapseMode="pin">
 
 			</com.google.android.material.appbar.MaterialToolbar>

+ 58 - 59
app/src/main/res/layout/activity_home.xml

@@ -1,61 +1,60 @@
-<androidx.constraintlayout.widget.ConstraintLayout
-	xmlns:android="http://schemas.android.com/apk/res/android"
-	xmlns:app="http://schemas.android.com/apk/res-auto"
-	xmlns:tools="http://schemas.android.com/tools"
-	android:id="@+id/main_content"
-	android:layout_width="match_parent"
-	android:layout_height="match_parent"
-	tools:context=".activities.HomeActivity"
-	android:visibility="invisible">
-
-	<com.google.android.material.appbar.AppBarLayout
-		android:id="@+id/appbar"
-		android:layout_width="match_parent"
-		android:layout_height="wrap_content"
-		app:layout_constraintEnd_toEndOf="parent"
-		app:layout_constraintStart_toStartOf="parent"
-		app:layout_constraintTop_toTopOf="parent">
-
-		<FrameLayout
-			android:layout_width="match_parent"
-			android:layout_height="wrap_content">
-
-			<include layout="@layout/toolbar_home"/>
-
-			<include layout="@layout/connection_indicator"/>
-
-		</FrameLayout>
-
-		<ch.threema.app.ui.OngoingCallNoticeView
-			android:id="@+id/ongoing_call_notice"
-			android:layout_width="match_parent"
-			android:layout_height="wrap_content"
-			android:animateLayoutChanges="true"
-			android:visibility="gone" />
-
-		<include layout="@layout/notice_sms_verification"/>
-
-	</com.google.android.material.appbar.AppBarLayout>
-
-	<include
-		layout="@layout/content_home"
-		android:layout_width="0dp"
-		android:layout_height="0dp"
-		app:layout_behavior="@string/appbar_scrolling_view_behavior"
-		app:layout_constraintBottom_toTopOf="@+id/bottom_navigation"
-		app:layout_constraintEnd_toEndOf="parent"
-		app:layout_constraintStart_toStartOf="parent"
-		app:layout_constraintTop_toBottomOf="@+id/appbar"/>
-
-	<com.google.android.material.bottomnavigation.BottomNavigationView
-		style="@style/Threema.BottomNavigationView"
-		android:id="@+id/bottom_navigation"
-		android:layout_width="match_parent"
-		android:layout_height="wrap_content"
-		android:layout_gravity="bottom"
-		app:layout_constraintBottom_toBottomOf="parent"
-		app:layout_constraintLeft_toLeftOf="parent"
-		app:layout_constraintRight_toRightOf="parent"
-		app:menu="@menu/activity_home_bottom_navigation"/>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/main_content"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:visibility="invisible"
+    tools:context=".activities.HomeActivity">
+
+    <com.google.android.material.appbar.AppBarLayout
+        android:id="@+id/appbar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <FrameLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <include layout="@layout/toolbar_home" />
+
+            <include layout="@layout/connection_indicator" />
+
+        </FrameLayout>
+
+        <ch.threema.app.ui.OngoingCallNoticeView
+            android:id="@+id/ongoing_call_notice"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:animateLayoutChanges="true"
+            android:visibility="gone" />
+
+        <include layout="@layout/notice_sms_verification" />
+
+    </com.google.android.material.appbar.AppBarLayout>
+
+    <include
+        layout="@layout/content_home"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:layout_behavior="@string/appbar_scrolling_view_behavior"
+        app:layout_constraintBottom_toTopOf="@+id/bottom_navigation"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/appbar" />
+
+    <com.google.android.material.bottomnavigation.BottomNavigationView
+        android:id="@+id/bottom_navigation"
+        style="@style/Threema.BottomNavigationView"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:menu="@menu/activity_home_bottom_navigation" />
 
 </androidx.constraintlayout.widget.ConstraintLayout>

+ 58 - 58
app/src/main/res/layout/activity_media_viewer.xml

@@ -1,70 +1,70 @@
 <?xml version="1.0" encoding="utf-8"?>
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-             xmlns:app="http://schemas.android.com/apk/res-auto"
-             android:layout_width="match_parent"
-             android:layout_height="match_parent"
-             android:background="@color/gallery_background">
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@color/gallery_background">
 
-	<ch.threema.app.ui.LockableViewPager
-		android:id="@+id/pager"
-		android:layout_width="match_parent"
-		android:layout_height="match_parent"/>
+    <ch.threema.app.ui.LockableViewPager
+        android:id="@+id/pager"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
 
-	<LinearLayout
-		android:id="@+id/session_loading"
-		android:layout_width="wrap_content"
-		android:layout_height="wrap_content"
-		android:layout_gravity="top|center_horizontal"
-		android:layout_marginTop="10dp"
-		android:orientation="horizontal"
-		android:visibility="gone">
+    <LinearLayout
+        android:id="@+id/session_loading"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="top|center_horizontal"
+        android:layout_marginTop="10dp"
+        android:orientation="horizontal"
+        android:visibility="gone">
 
-		<com.google.android.material.progressindicator.CircularProgressIndicator
-			android:id="@+id/progress_bar"
-			android:indeterminate="true"
-			style="@style/Widget.Material3.CircularProgressIndicator.Small"
-			android:layout_width="wrap_content"
-			android:layout_height="wrap_content"
-			android:layout_marginTop="2dp"/>
+        <com.google.android.material.progressindicator.CircularProgressIndicator
+            android:id="@+id/progress_bar"
+            style="@style/Widget.Material3.CircularProgressIndicator.Small"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="2dp"
+            android:indeterminate="true" />
 
-		<TextView
-			android:layout_width="wrap_content"
-			android:layout_height="wrap_content"
-			android:layout_marginLeft="10sp"
-			android:text="@string/decoding_message"
-			android:textColor="@android:color/white"/>
-	</LinearLayout>
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="10sp"
+            android:text="@string/decoding_message"
+            android:textColor="@android:color/white" />
+    </LinearLayout>
 
-	<com.google.android.material.card.MaterialCardView
-		style="@style/Threema.CardView.MediaViewerCaption"
-		android:id="@+id/caption_container"
-		android:layout_width="wrap_content"
-		android:layout_height="wrap_content"
-		android:layout_gravity="bottom|center_horizontal"
-		app:contentPaddingTop="@dimen/mediaviewer_caption_container_padding_vertical"
-		app:contentPaddingBottom="@dimen/mediaviewer_caption_container_padding_vertical"
-		app:contentPaddingLeft="16dp"
-		app:contentPaddingRight="16dp"
-		android:visibility="gone">
+    <com.google.android.material.card.MaterialCardView
+        android:id="@+id/caption_container"
+        style="@style/Threema.CardView.MediaViewerCaption"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom|center_horizontal"
+        android:visibility="gone"
+        app:contentPaddingBottom="@dimen/mediaviewer_caption_container_padding_vertical"
+        app:contentPaddingLeft="16dp"
+        app:contentPaddingRight="16dp"
+        app:contentPaddingTop="@dimen/mediaviewer_caption_container_padding_vertical">
 
-		<ch.threema.app.emojis.EmojiTextView
-			android:id="@+id/caption"
-			android:layout_width="wrap_content"
-			android:layout_height="wrap_content"
-			android:layout_gravity="center_horizontal"
-			android:text=""
-			android:ellipsize="end"
-			android:textColor="@android:color/white"
-			android:textSize="@dimen/mediaviewer_caption_text_size"/>
+        <ch.threema.app.emojis.EmojiTextView
+            android:id="@+id/caption"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:ellipsize="end"
+            android:text=""
+            android:textColor="@android:color/white"
+            android:textSize="@dimen/mediaviewer_caption_text_size" />
 
-	</com.google.android.material.card.MaterialCardView>
+    </com.google.android.material.card.MaterialCardView>
 
-	<com.google.android.material.appbar.MaterialToolbar
-		android:id="@+id/toolbar"
-		android:layout_width="match_parent"
-		android:layout_height="?attr/actionBarSize"
-		android:background="#77000000"
-		android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
-		app:popupTheme="@style/ThemeOverlay.AppCompat"/>
+    <com.google.android.material.appbar.MaterialToolbar
+        android:id="@+id/toolbar"
+        android:layout_width="match_parent"
+        android:layout_height="?attr/actionBarSize"
+        android:background="#77000000"
+        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
+        app:popupTheme="@style/Threema.PopupMenuStyle.Overflow" />
 
 </FrameLayout>

+ 7 - 8
app/src/main/res/layout/content_home.xml

@@ -1,12 +1,11 @@
 <?xml version="1.0" encoding="utf-8"?>
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-             xmlns:app="http://schemas.android.com/apk/res-auto"
-             xmlns:tools="http://schemas.android.com/tools"
-             android:layout_width="match_parent"
-             android:layout_height="match_parent"
-             app:layout_behavior="@string/appbar_scrolling_view_behavior"
-             tools:showIn="@layout/activity_home"
-             android:padding="1dp"
-             android:id="@+id/home_container">
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/home_container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    app:layout_behavior="@string/appbar_scrolling_view_behavior"
+    tools:showIn="@layout/activity_home">
 
 </FrameLayout>

+ 44 - 44
app/src/main/res/layout/conversation_bubble_footer_groupack_recv.xml

@@ -1,52 +1,52 @@
 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-	xmlns:app="http://schemas.android.com/apk/res-auto"
-	android:id="@+id/groupack_container"
-	android:layout_width="wrap_content"
-	android:layout_height="match_parent"
-	android:orientation="horizontal"
-	android:layout_marginRight="3dp"
-	android:visibility="gone">
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/groupack_container"
+    android:layout_width="wrap_content"
+    android:layout_height="match_parent"
+    android:layout_marginRight="3dp"
+    android:orientation="horizontal"
+    android:visibility="gone">
 
-	<ImageView
-		android:id="@+id/groupack_thumbsup"
-		android:layout_width="18dp"
-		android:layout_height="18dp"
-		android:padding="2dp"
-		android:layout_gravity="center_vertical"
-		android:src="@drawable/ic_thumb_up_grey600_24dp"
-		app:tint="@color/material_green"
-		android:contentDescription="@string/message_acknowledged" />
+    <ImageView
+        android:id="@+id/groupack_thumbsup"
+        android:layout_width="18dp"
+        android:layout_height="18dp"
+        android:layout_gravity="center_vertical"
+        android:contentDescription="@string/message_acknowledged"
+        android:padding="2dp"
+        android:src="@drawable/ic_thumb_up_grey600_24dp"
+        app:tint="@color/material_green" />
 
-	<TextView
-		android:id="@+id/groupack_thumbsup_count"
-		android:layout_width="wrap_content"
-		android:layout_height="match_parent"
-		android:layout_marginRight="3dp"
-		android:layout_gravity="bottom"
-		android:ellipsize="none"
-		android:singleLine="true"
-		android:textColor="@color/material_green"
-		android:textSize="?attr/font_medium" />
+    <TextView
+        android:id="@+id/groupack_thumbsup_count"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_gravity="bottom"
+        android:layout_marginRight="3dp"
+        android:ellipsize="none"
+        android:singleLine="true"
+        android:textColor="@color/material_green"
+        android:textSize="?attr/font_medium" />
 
-	<ImageView
-		android:id="@+id/groupack_thumbsdown"
-		android:layout_width="18dp"
-		android:layout_height="18dp"
-		android:padding="2dp"
-		android:layout_gravity="center_vertical"
-		android:src="@drawable/ic_thumb_down_grey600_24dp"
-		app:tint="@color/material_orange"
-		android:contentDescription="@string/message_declined" />
+    <ImageView
+        android:id="@+id/groupack_thumbsdown"
+        android:layout_width="18dp"
+        android:layout_height="18dp"
+        android:layout_gravity="center_vertical"
+        android:contentDescription="@string/message_declined"
+        android:padding="2dp"
+        android:src="@drawable/ic_thumb_down_grey600_24dp"
+        app:tint="@color/material_orange" />
 
-	<TextView
-		android:id="@+id/groupack_thumbsdown_count"
-		android:layout_width="wrap_content"
-		android:layout_height="match_parent"
-		android:layout_gravity="bottom"
-		android:ellipsize="none"
-		android:singleLine="true"
-		android:textColor="@color/material_orange"
-		android:textSize="?attr/font_medium" />
+    <TextView
+        android:id="@+id/groupack_thumbsdown_count"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_gravity="bottom"
+        android:ellipsize="none"
+        android:singleLine="true"
+        android:textColor="@color/material_orange"
+        android:textSize="?attr/font_medium" />
 
 </LinearLayout>

+ 67 - 59
app/src/main/res/layout/conversation_bubble_footer_recv.xml

@@ -1,69 +1,77 @@
 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-	xmlns:app="http://schemas.android.com/apk/res-auto"
-			  android:id="@+id/indicator_container"
-			  android:layout_width="match_parent"
-			  android:layout_height="wrap_content"
-			  android:minHeight="16sp"
-			  android:layout_marginTop="2dp"
-			  android:paddingLeft="@dimen/chat_bubble_margin_start"
-			  android:paddingRight="@dimen/chat_bubble_margin_end"
-			  android:orientation="horizontal"
-			  android:gravity="bottom"
-			  android:layout_gravity="bottom">
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/indicator_container"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_gravity="bottom"
+    android:layout_marginTop="2dp"
+    android:gravity="bottom"
+    android:minHeight="16sp"
+    android:orientation="horizontal"
+    android:paddingLeft="@dimen/chat_bubble_margin_start"
+    android:paddingRight="@dimen/chat_bubble_margin_end"
+    tools:background="@color/bubble_receive">
 
-	<TextView
-		style="@style/Threema.Bubble.Text.Footer"
-		android:id="@+id/edited_text"
-		android:layout_width="0dp"
-		android:layout_height="match_parent"
-		android:layout_gravity="bottom"
-		android:layout_weight="1.0"
-		android:layout_marginRight="4dp"
-		android:maxLines="1"
-		android:ellipsize="end"
-		android:text="@string/edited"
-		android:visibility="gone" />
+    <TextView
+        android:id="@+id/edited_text"
+        style="@style/Threema.Bubble.Text.Footer"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_gravity="bottom"
+        android:layout_marginRight="4dp"
+        android:layout_weight="1.0"
+        android:ellipsize="end"
+        android:maxLines="1"
+        android:text="@string/edited"
+        android:visibility="gone"
+        tools:visibility="visible" />
 
-	<ImageView
-			android:id="@+id/delivered_indicator"
-			android:layout_width="18dp"
-			android:layout_height="18dp"
-			android:layout_gravity="center_vertical"
-			android:paddingRight="3dp"
-			android:visibility="gone"
-			app:tint="@color/bubble_text_colorstatelist" />
+    <ImageView
+        android:id="@+id/delivered_indicator"
+        android:layout_width="18dp"
+        android:layout_height="18dp"
+        android:layout_gravity="center_vertical"
+        android:paddingRight="3dp"
+        android:visibility="gone"
+        app:tint="@color/bubble_receive_text_colorstatelist"
+        tools:src="@drawable/ic_baseline_hearing_24"
+        tools:visibility="visible" />
 
-	<include layout="@layout/conversation_bubble_footer_groupack_recv"/>
+    <include layout="@layout/conversation_bubble_footer_groupack_recv" />
 
-	<TextView
-			style="@style/Threema.Bubble.Text.Footer"
-			android:id="@+id/date_view"
-			android:layout_width="wrap_content"
-			android:layout_height="match_parent"
-			android:layout_gravity="bottom"
-			android:singleLine="true"
-			android:ellipsize="none" />
+    <TextView
+        android:id="@+id/date_view"
+        style="@style/Threema.Bubble.Text.Footer"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_gravity="bottom"
+        android:ellipsize="none"
+        android:singleLine="true"
+        tools:text="11.11.2024" />
 
-	<ImageView
-		android:id="@+id/date_prefix_icon"
-		app:srcCompat="@drawable/ic_av_timer_grey600_18dp"
-		app:tint="@color/bubble_text_colorstatelist"
-		android:layout_width="18dp"
-		android:layout_height="18dp"
-		android:padding="3dp"
-		android:layout_gravity="center_vertical"
-		android:visibility="gone"
-		android:importantForAccessibility="no" />
+    <ImageView
+        android:id="@+id/date_prefix_icon"
+        android:layout_width="18dp"
+        android:layout_height="18dp"
+        android:layout_gravity="center_vertical"
+        android:importantForAccessibility="no"
+        android:padding="3dp"
+        android:visibility="gone"
+        app:srcCompat="@drawable/ic_av_timer_grey600_18dp"
+        app:tint="@color/bubble_receive_text_colorstatelist"
+        tools:visibility="visible" />
 
-	<ImageView
-		android:id="@+id/star_icon"
-		app:srcCompat="@drawable/ic_star_golden_24dp"
-		android:layout_width="18dp"
-		android:layout_height="18dp"
-		android:padding="3dp"
-		android:layout_gravity="center_vertical"
-		android:visibility="gone"
-		android:contentDescription="@string/starred_message" />
+    <ImageView
+        android:id="@+id/star_icon"
+        android:layout_width="18dp"
+        android:layout_height="18dp"
+        android:layout_gravity="center_vertical"
+        android:contentDescription="@string/starred_message"
+        android:padding="3dp"
+        android:visibility="gone"
+        app:srcCompat="@drawable/ic_star_golden_24dp"
+        tools:visibility="visible" />
 
 </LinearLayout>

+ 18 - 18
app/src/main/res/layout/conversation_bubble_footer_send.xml

@@ -14,59 +14,59 @@
     android:paddingRight="@dimen/chat_bubble_margin_start">
 
     <TextView
-        style="@style/Threema.Bubble.Text.Footer"
         android:id="@+id/tap_to_resend"
+        style="@style/Threema.Bubble.Text.Footer"
         android:layout_width="wrap_content"
         android:layout_height="match_parent"
         android:layout_gravity="bottom"
-        android:layout_weight="1.0"
         android:layout_marginRight="4dp"
-        android:maxLines="1"
+        android:layout_weight="1.0"
         android:ellipsize="end"
+        android:maxLines="1"
         android:text="@string/tap_to_resend"
         android:textColor="@color/material_red"
         android:visibility="gone"
-        tools:visibility="visible"/>
+        tools:visibility="visible" />
 
     <TextView
-        style="@style/Threema.Bubble.Text.Footer"
         android:id="@+id/edited_text"
+        style="@style/Threema.Bubble.Text.Footer"
         android:layout_width="wrap_content"
         android:layout_height="match_parent"
         android:layout_gravity="bottom"
-        android:layout_weight="1.0"
         android:layout_marginRight="4dp"
-        android:maxLines="1"
+        android:layout_weight="1.0"
         android:ellipsize="end"
+        android:maxLines="1"
         android:text="@string/edited"
         android:visibility="gone"
-        tools:visibility="visible"/>
+        tools:visibility="visible" />
 
     <ImageView
         android:id="@+id/star_icon"
-        app:srcCompat="@drawable/ic_star_golden_24dp"
         android:layout_width="18dp"
         android:layout_height="18dp"
-        android:padding="3dp"
         android:layout_gravity="center_vertical"
-        android:visibility="gone"
         android:contentDescription="@string/starred_message"
-        tools:visibility="visible"/>
+        android:padding="3dp"
+        android:visibility="gone"
+        app:srcCompat="@drawable/ic_star_golden_24dp"
+        tools:visibility="visible" />
 
     <ImageView
         android:id="@+id/date_prefix_icon"
-        app:srcCompat="@drawable/ic_av_timer_grey600_18dp"
-        app:tint="@color/bubble_text_colorstatelist"
         android:layout_width="18dp"
         android:layout_height="18dp"
-        android:padding="3dp"
         android:layout_gravity="center_vertical"
+        android:importantForAccessibility="no"
+        android:padding="3dp"
         android:visibility="gone"
-        android:importantForAccessibility="no" />
+        app:srcCompat="@drawable/ic_av_timer_grey600_18dp"
+        app:tint="@color/bubble_send_text_colorstatelist" />
 
     <TextView
-        style="@style/Threema.Bubble.Text.Footer"
         android:id="@+id/date_view"
+        style="@style/Threema.Bubble.Text.Footer"
         android:layout_width="wrap_content"
         android:layout_height="match_parent"
         android:layout_gravity="bottom"
@@ -82,7 +82,7 @@
         android:layout_gravity="center_vertical"
         android:paddingLeft="3dp"
         android:visibility="gone"
-        app:tint="@color/bubble_text_colorstatelist"
+        app:tint="@color/bubble_send_text_colorstatelist"
         tools:visibility="visible" />
 
 </LinearLayout>

+ 23 - 21
app/src/main/res/layout/conversation_bubble_header.xml

@@ -1,26 +1,28 @@
 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-			  android:id="@+id/group_sender_view"
-			  android:layout_height="wrap_content"
-			  android:layout_width="match_parent"
-			  android:minHeight="16sp"
-			  android:paddingLeft="@dimen/chat_bubble_margin_start"
-			  android:paddingRight="@dimen/chat_bubble_margin_end"
-			  android:layout_marginTop="-2dp"
-			  android:layout_marginBottom="3dp"
-			  android:layout_gravity="left|top"
-			  android:gravity="center_vertical"
-			  android:visibility="gone">
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/group_sender_view"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_gravity="left|top"
+    android:layout_marginTop="-2dp"
+    android:layout_marginBottom="3dp"
+    android:gravity="center_vertical"
+    android:minHeight="16sp"
+    android:paddingLeft="@dimen/chat_bubble_margin_start"
+    android:paddingRight="@dimen/chat_bubble_margin_end"
+    android:visibility="gone"
+    tools:visibility="visible">
 
-	<ch.threema.app.emojis.EmojiTextView
-			style="@style/Threema.Bubble.Text.Footer"
-			android:id="@+id/group_sender_name"
-			android:layout_width="wrap_content"
-			android:layout_height="wrap_content"
-			android:layout_gravity="center_vertical"
-			android:textStyle="bold"
-			android:ellipsize="end"
-			android:maxLines="1"
-			/>
+    <ch.threema.app.emojis.EmojiTextView
+        android:id="@+id/group_sender_name"
+        style="@style/Threema.Bubble.Text.Footer"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:ellipsize="end"
+        android:maxLines="1"
+        android:textStyle="bold"
+        tools:text="@tools:sample/full_names"/>
 
 </LinearLayout>

+ 68 - 66
app/src/main/res/layout/conversation_list_item_audio.xml

@@ -1,77 +1,79 @@
 <?xml version="1.0" encoding="utf-8"?>
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-	xmlns:app="http://schemas.android.com/apk/res-auto"
-	android:layout_width="fill_parent"
-                android:layout_height="wrap_content"
-                android:layout_gravity="center"
-                android:minHeight="52dp"
-				android:contentDescription="@string/audio_placeholder">
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center"
+    android:contentDescription="@string/audio_placeholder"
+    android:minHeight="52dp"
+    tools:background="@color/bubble_receive">
 
-	<ch.threema.app.ui.ControllerView
-		android:id="@+id/controller"
-		android:layout_width="@dimen/avatar_size_small"
-		android:layout_height="@dimen/avatar_size_small"
-		android:layout_centerVertical="true"
-		android:layout_gravity="center"/>
+    <ch.threema.app.ui.ControllerView
+        android:id="@+id/controller"
+        android:layout_width="@dimen/avatar_size_small"
+        android:layout_height="@dimen/avatar_size_small"
+        android:layout_centerVertical="true"
+        android:layout_gravity="center" />
 
-	<ch.threema.app.ui.AudioProgressBarView
-		style="@style/SeekBar.Audio"
-		android:id="@+id/seek"
-		android:layout_width="fill_parent"
-		android:layout_height="48dp"
-		android:layout_marginLeft="50dp"
-		android:layout_marginRight="10dp"
-		android:layout_centerVertical="true"
-		android:paddingLeft="6dp"
-		android:paddingRight="6dp"
-		android:layout_toLeftOf="@+id/document_size_view"
-		android:enabled="false"
-		android:visibility="visible"
-		app:barColor="@color/bubble_text_colorstatelist"
-		app:barColorActivated="?attr/colorPrimary"
-		app:barWidth="3dp"
-		app:spaceWidth="2dp"
-		app:barMinHeight="2dp"
-		app:barHeight="48dp"/>
+    <ch.threema.app.ui.AudioProgressBarView
+        android:id="@+id/seek"
+        style="@style/SeekBar.Audio"
+        android:layout_width="fill_parent"
+        android:layout_height="48dp"
+        android:layout_centerVertical="true"
+        android:layout_marginLeft="50dp"
+        android:layout_marginRight="10dp"
+        android:layout_toLeftOf="@+id/document_size_view"
+        android:enabled="false"
+        android:paddingLeft="6dp"
+        android:paddingRight="6dp"
+        android:visibility="visible"
+        app:barColor="@color/bubble_send_text_colorstatelist"
+        app:barColorActivated="?attr/colorPrimary"
+        app:barHeight="48dp"
+        app:barMinHeight="2dp"
+        app:barWidth="3dp"
+        app:spaceWidth="2dp" />
 
-	<TextView
-		style="@style/Threema.Bubble.Text.Body.Small"
-		android:id="@+id/document_size_view"
-		android:layout_width="wrap_content"
-		android:layout_height="wrap_content"
-		android:layout_marginRight="4dp"
-		android:layout_toLeftOf="@+id/speed_container"
-		android:layout_centerVertical="true"
-		android:text="00:00"/>
+    <TextView
+        android:id="@+id/document_size_view"
+        style="@style/Threema.Bubble.Text.Body.Small"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerVertical="true"
+        android:layout_marginRight="4dp"
+        android:layout_toLeftOf="@+id/speed_container"
+        android:text="00:00" />
 
-	<FrameLayout
-		android:id="@+id/speed_container"
-		android:layout_width="wrap_content"
-        android:minWidth="56dp"
-		android:layout_height="wrap_content"
-		android:layout_alignParentRight="true"
-		android:layout_centerVertical="true">
+    <FrameLayout
+        android:id="@+id/speed_container"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentRight="true"
+        android:layout_centerVertical="true"
+        android:minWidth="56dp">
 
-		<com.google.android.material.chip.Chip
-			android:id="@+id/read_on_button"
-			style="@style/Threema.Chip.AudioMessage"
-			android:layout_width="wrap_content"
-			android:layout_height="wrap_content"
-			android:layout_gravity="center"
-			android:visibility="gone"/>
+        <com.google.android.material.chip.Chip
+            android:id="@+id/read_on_button"
+            style="@style/Threema.Chip.AudioMessage"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:visibility="gone"
+            tools:text="1.5x"/>
 
-		<ImageView
-			android:layout_width="@dimen/avatar_size_small"
-			android:layout_height="@dimen/avatar_size_small"
-			android:layout_gravity="center_vertical|right"
-			android:id="@+id/message_type_button"
-			app:srcCompat="@drawable/ic_microphone_outline"
-			app:tint="@color/bubble_text_colorstatelist"
-			android:padding="8dp"
-			android:clickable="false"
-			android:focusable="false"
-			android:importantForAccessibility="no" />
+        <ImageView
+            android:id="@+id/audio_message_icon"
+            android:layout_width="@dimen/avatar_size_small"
+            android:layout_height="@dimen/avatar_size_small"
+            android:layout_gravity="center_vertical|right"
+            android:clickable="false"
+            android:focusable="false"
+            android:importantForAccessibility="no"
+            android:padding="8dp"
+            app:srcCompat="@drawable/ic_microphone_outline" />
 
-	</FrameLayout>
+    </FrameLayout>
 
 </RelativeLayout>

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov