Эх сурвалжийг харах

Merge pull request #2217 from nextcloud/feature/250/server-theme

🎨 Server theming
Andy Scherzinger 2 жил өмнө
parent
commit
ce71746461
74 өөрчлөгдсөн 1456 нэмэгдсэн , 368 устгасан
  1. 1 0
      app/build.gradle
  2. 13 0
      app/src/main/java/com/nextcloud/talk/activities/TakePhotoActivity.java
  3. 6 1
      app/src/main/java/com/nextcloud/talk/adapters/items/ContactItem.java
  4. 12 27
      app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java
  5. 5 1
      app/src/main/java/com/nextcloud/talk/adapters/items/GenericTextHeaderItem.java
  6. 5 4
      app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt
  7. 3 1
      app/src/main/java/com/nextcloud/talk/adapters/items/MessagesTextHeaderItem.kt
  8. 5 0
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt
  9. 29 13
      app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt
  10. 12 0
      app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java
  11. 28 11
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt
  12. 34 11
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt
  13. 37 10
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt
  14. 4 1
      app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt
  15. 12 1
      app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
  16. 11 11
      app/src/main/java/com/nextcloud/talk/controllers/ContactsController.kt
  17. 63 31
      app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt
  18. 16 8
      app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java
  19. 36 32
      app/src/main/java/com/nextcloud/talk/controllers/ProfileController.kt
  20. 37 2
      app/src/main/java/com/nextcloud/talk/controllers/SettingsController.kt
  21. 16 10
      app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/EntryMenuController.kt
  22. 5 0
      app/src/main/java/com/nextcloud/talk/data/user/UsersDao.kt
  23. 2 0
      app/src/main/java/com/nextcloud/talk/data/user/UsersRepository.kt
  24. 5 0
      app/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.kt
  25. 8 2
      app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchActivity.kt
  26. 5 1
      app/src/main/java/com/nextcloud/talk/models/json/capabilities/ThemingCapability.kt
  27. 4 1
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionViewHolder.kt
  28. 4 2
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionsAdapter.kt
  29. 5 1
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultHeaderViewHolder.kt
  30. 3 1
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultsAdapter.kt
  31. 23 1
      app/src/main/java/com/nextcloud/talk/polls/ui/PollCreateDialogFragment.kt
  32. 13 2
      app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt
  33. 14 0
      app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt
  34. 6 2
      app/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.kt
  35. 6 3
      app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsAdapter.kt
  36. 4 6
      app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsListViewHolder.kt
  37. 11 3
      app/src/main/java/com/nextcloud/talk/shareditems/activities/SharedItemsActivity.kt
  38. 7 4
      app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsAdapter.kt
  39. 4 2
      app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsGridViewHolder.kt
  40. 4 3
      app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsListViewHolder.kt
  41. 4 14
      app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsViewHolder.kt
  42. 24 41
      app/src/main/java/com/nextcloud/talk/ui/dialog/AudioOutputDialog.kt
  43. 7 0
      app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java
  44. 28 27
      app/src/main/java/com/nextcloud/talk/ui/dialog/SetStatusDialogFragment.kt
  45. 4 1
      app/src/main/java/com/nextcloud/talk/ui/dialog/ShowReactionsDialog.kt
  46. 8 8
      app/src/main/java/com/nextcloud/talk/ui/dialog/SortingOrderDialogFragment.java
  47. 53 0
      app/src/main/java/com/nextcloud/talk/ui/theme/ServerTheme.kt
  48. 48 0
      app/src/main/java/com/nextcloud/talk/ui/theme/ServerThemeImpl.kt
  49. 31 0
      app/src/main/java/com/nextcloud/talk/ui/theme/ServerThemeProvider.kt
  50. 65 0
      app/src/main/java/com/nextcloud/talk/ui/theme/ServerThemeProviderImpl.kt
  51. 44 0
      app/src/main/java/com/nextcloud/talk/ui/theme/ThemeModule.kt
  52. 364 0
      app/src/main/java/com/nextcloud/talk/ui/theme/ViewThemeUtils.kt
  53. 8 3
      app/src/main/java/com/nextcloud/talk/users/UserManager.kt
  54. 16 26
      app/src/main/java/com/nextcloud/talk/utils/DrawableUtils.kt
  55. 49 0
      app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderImpl.kt
  56. 1 1
      app/src/main/java/com/nextcloud/talk/utils/database/user/UserModule.kt
  57. 81 0
      app/src/main/java/com/nextcloud/talk/utils/ui/ColorUtil.kt
  58. 33 0
      app/src/main/java/com/nextcloud/talk/utils/ui/PlatformThemeUtil.kt
  59. 1 0
      app/src/main/res/layout/activity_take_picture.xml
  60. 2 0
      app/src/main/res/layout/controller_conversation_info.xml
  61. 2 2
      app/src/main/res/layout/controller_conversations_rv.xml
  62. 2 2
      app/src/main/res/layout/controller_entry_menu.xml
  63. 2 1
      app/src/main/res/layout/controller_profile.xml
  64. 7 1
      app/src/main/res/layout/controller_settings.xml
  65. 1 0
      app/src/main/res/layout/current_account_item.xml
  66. 1 0
      app/src/main/res/layout/dialog_choose_account.xml
  67. 3 0
      app/src/main/res/layout/dialog_poll_create.xml
  68. 2 2
      app/src/main/res/layout/item_custom_outcoming_location_message.xml
  69. 4 4
      app/src/main/res/layout/item_custom_outcoming_poll_message.xml
  70. 1 0
      app/src/main/res/layout/notification_settings_item.xml
  71. 33 21
      app/src/main/res/layout/user_info_details_table_item.xml
  72. 4 0
      app/src/main/res/values-night/colors.xml
  73. 4 5
      app/src/main/res/values/colors.xml
  74. 1 0
      app/src/main/res/values/dimens.xml

+ 1 - 0
app/build.gradle

@@ -79,6 +79,7 @@ android {
             disable 'InvalidPackage'
             disable 'MissingTranslation'
             disable 'VectorPath'
+            disable 'UnusedQuantity'
         }
 
         javaCompileOptions {

+ 13 - 0
app/src/main/java/com/nextcloud/talk/activities/TakePhotoActivity.java

@@ -41,8 +41,10 @@ import android.widget.Toast;
 
 import com.google.common.util.concurrent.ListenableFuture;
 import com.nextcloud.talk.R;
+import com.nextcloud.talk.application.NextcloudTalkApplication;
 import com.nextcloud.talk.databinding.ActivityTakePictureBinding;
 import com.nextcloud.talk.models.TakePictureViewModel;
+import com.nextcloud.talk.ui.theme.ViewThemeUtils;
 import com.nextcloud.talk.utils.BitmapShrinker;
 import com.nextcloud.talk.utils.FileUtils;
 
@@ -52,6 +54,8 @@ import java.util.Date;
 import java.util.Locale;
 import java.util.concurrent.ExecutionException;
 
+import javax.inject.Inject;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.OptIn;
@@ -66,9 +70,11 @@ import androidx.camera.lifecycle.ProcessCameraProvider;
 import androidx.core.content.ContextCompat;
 import androidx.exifinterface.media.ExifInterface;
 import androidx.lifecycle.ViewModelProvider;
+import autodagger.AutoInjector;
 
 import static com.nextcloud.talk.utils.Mimetype.IMAGE_JPEG;
 
+@AutoInjector(NextcloudTalkApplication.class)
 public class TakePhotoActivity extends AppCompatActivity {
 
     private static final String TAG = TakePhotoActivity.class.getSimpleName();
@@ -86,15 +92,22 @@ public class TakePhotoActivity extends AppCompatActivity {
 
     private Camera camera;
 
+    @Inject
+    ViewThemeUtils viewThemeUtils;
+
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
 
         binding = ActivityTakePictureBinding.inflate(getLayoutInflater());
         viewModel = new ViewModelProvider(this).get(TakePictureViewModel.class);
 
         setContentView(binding.getRoot());
 
+        viewThemeUtils.themeFAB(binding.takePhoto);
+        viewThemeUtils.colorMaterialButtonBackground(binding.send);
+
         cameraProviderFuture = ProcessCameraProvider.getInstance(this);
         cameraProviderFuture.addListener(() -> {
             try {

+ 6 - 1
app/src/main/java/com/nextcloud/talk/adapters/items/ContactItem.java

@@ -35,6 +35,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication;
 import com.nextcloud.talk.data.user.model.User;
 import com.nextcloud.talk.databinding.RvItemContactBinding;
 import com.nextcloud.talk.models.json.participants.Participant;
+import com.nextcloud.talk.ui.theme.ViewThemeUtils;
 import com.nextcloud.talk.utils.ApiUtils;
 import com.nextcloud.talk.utils.DisplayUtils;
 
@@ -59,14 +60,17 @@ public class ContactItem extends AbstractFlexibleItem<ContactItem.ContactItemVie
     private final Participant participant;
     private final User user;
     private GenericTextHeaderItem header;
+    private final ViewThemeUtils viewThemeUtils;
     public boolean isOnline = true;
 
     public ContactItem(Participant participant,
                        User user,
-                       GenericTextHeaderItem genericTextHeaderItem) {
+                       GenericTextHeaderItem genericTextHeaderItem,
+                       ViewThemeUtils viewThemeUtils) {
         this.participant = participant;
         this.user = user;
         this.header = genericTextHeaderItem;
+        this.viewThemeUtils = viewThemeUtils;
     }
 
     @Override
@@ -108,6 +112,7 @@ public class ContactItem extends AbstractFlexibleItem<ContactItem.ContactItemVie
         holder.binding.avatarDraweeView.setController(null);
 
         if (participant.getSelected()) {
+            viewThemeUtils.colorImageView(holder.binding.checkedImageView);
             holder.binding.checkedImageView.setVisibility(View.VISIBLE);
         } else {
             holder.binding.checkedImageView.setVisibility(View.GONE);

+ 12 - 27
app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java

@@ -27,7 +27,6 @@ package com.nextcloud.talk.adapters.items;
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.res.ColorStateList;
-import android.graphics.Color;
 import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.LayerDrawable;
@@ -46,6 +45,7 @@ import com.nextcloud.talk.models.json.chat.ChatMessage;
 import com.nextcloud.talk.models.json.conversations.Conversation;
 import com.nextcloud.talk.models.json.status.Status;
 import com.nextcloud.talk.ui.StatusDrawable;
+import com.nextcloud.talk.ui.theme.ViewThemeUtils;
 import com.nextcloud.talk.utils.ApiUtils;
 import com.nextcloud.talk.utils.DisplayUtils;
 import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew;
@@ -76,22 +76,22 @@ public class ConversationItem extends AbstractFlexibleItem<ConversationItem.Conv
     private final Context context;
     private GenericTextHeaderItem header;
     private final Status status;
+    private final ViewThemeUtils viewThemeUtils;
 
 
-    public ConversationItem(Conversation conversation, User user, Context activityContext, Status status) {
+    public ConversationItem(Conversation conversation, User user, Context activityContext, Status status, final ViewThemeUtils viewThemeUtils) {
         this.conversation = conversation;
         this.user = user;
         this.context = activityContext;
         this.status = status;
+        this.viewThemeUtils = viewThemeUtils;
     }
 
     public ConversationItem(Conversation conversation, User user,
-                            Context activityContext, GenericTextHeaderItem genericTextHeaderItem, Status status) {
-        this.conversation = conversation;
-        this.user = user;
-        this.context = activityContext;
+                            Context activityContext, GenericTextHeaderItem genericTextHeaderItem, Status status,
+                            final ViewThemeUtils viewThemeUtils) {
+        this(conversation, user, activityContext, status, viewThemeUtils);
         this.header = genericTextHeaderItem;
-        this.status = status;
     }
 
     @Override
@@ -146,11 +146,7 @@ public class ConversationItem extends AbstractFlexibleItem<ConversationItem.Conv
         if (adapter.hasFilter()) {
             FlexibleUtils.highlightText(holder.binding.dialogName, conversation.getDisplayName(),
                                         String.valueOf(adapter.getFilter(String.class)),
-                                        NextcloudTalkApplication
-                                            .Companion
-                                            .getSharedApplication()
-                                            .getResources()
-                                            .getColor(R.color.colorPrimary));
+                                        viewThemeUtils.getElementColor(holder.binding.dialogName.getContext()));
         } else {
             holder.binding.dialogName.setText(conversation.getDisplayName());
         }
@@ -171,29 +167,18 @@ public class ConversationItem extends AbstractFlexibleItem<ConversationItem.Conv
             int lightBubbleTextColor = ContextCompat.getColor(
                 context,
                 R.color.conversation_unread_bubble_text);
-            ColorStateList lightBubbleStrokeColor = ColorStateList.valueOf(
-                ContextCompat.getColor(context,
-                                       R.color.colorPrimary));
 
             if (conversation.getType() == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
-                holder.binding.dialogUnreadBubble.setChipBackgroundColorResource(R.color.colorPrimary);
-                holder.binding.dialogUnreadBubble.setTextColor(Color.WHITE);
+                viewThemeUtils.colorChipBackground(holder.binding.dialogUnreadBubble);
             } else if (conversation.getUnreadMention()) {
                 if (CapabilitiesUtilNew.hasSpreedFeatureCapability(user, "direct-mention-flag")) {
                     if (conversation.getUnreadMentionDirect()) {
-                        holder.binding.dialogUnreadBubble.setChipBackgroundColorResource(R.color.colorPrimary);
-                        holder.binding.dialogUnreadBubble.setTextColor(Color.WHITE);
+                        viewThemeUtils.colorChipBackground(holder.binding.dialogUnreadBubble);
                     } else {
-                        holder.binding.dialogUnreadBubble.setChipBackgroundColorResource(R.color.bg_default);
-                        holder.binding.dialogUnreadBubble.setTextColor(ContextCompat.getColor(
-                            context,
-                            R.color.colorPrimary));
-                        holder.binding.dialogUnreadBubble.setChipStrokeWidth(6.0f);
-                        holder.binding.dialogUnreadBubble.setChipStrokeColor(lightBubbleStrokeColor);
+                        viewThemeUtils.colorChipOutlined(holder.binding.dialogUnreadBubble, 6.0f);
                     }
                 } else {
-                    holder.binding.dialogUnreadBubble.setChipBackgroundColorResource(R.color.colorPrimary);
-                    holder.binding.dialogUnreadBubble.setTextColor(Color.WHITE);
+                    viewThemeUtils.colorChipBackground(holder.binding.dialogUnreadBubble);
                 }
             } else {
                 holder.binding.dialogUnreadBubble.setChipBackgroundColor(lightBubbleFillColor);

+ 5 - 1
app/src/main/java/com/nextcloud/talk/adapters/items/GenericTextHeaderItem.java

@@ -27,6 +27,7 @@ import android.view.View;
 
 import com.nextcloud.talk.R;
 import com.nextcloud.talk.databinding.RvItemTitleHeaderBinding;
+import com.nextcloud.talk.ui.theme.ViewThemeUtils;
 
 import java.util.List;
 
@@ -39,12 +40,14 @@ public class GenericTextHeaderItem extends AbstractHeaderItem<GenericTextHeaderI
     private static final String TAG = "GenericTextHeaderItem";
 
     private final String title;
+    private final ViewThemeUtils viewThemeUtils;
 
-    public GenericTextHeaderItem(String title) {
+    public GenericTextHeaderItem(String title, ViewThemeUtils viewThemeUtils) {
         super();
         setHidden(false);
         setSelectable(false);
         this.title = title;
+        this.viewThemeUtils = viewThemeUtils;
     }
 
     public String getModel() {
@@ -71,6 +74,7 @@ public class GenericTextHeaderItem extends AbstractHeaderItem<GenericTextHeaderI
             Log.d(TAG, "We have payloads, so ignoring!");
         } else {
             holder.binding.titleTextView.setText(title);
+            viewThemeUtils.colorTextViewElement(holder.binding.titleTextView);
         }
     }
 

+ 5 - 4
app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt

@@ -24,12 +24,12 @@ package com.nextcloud.talk.adapters.items
 import android.content.Context
 import android.text.SpannableString
 import android.view.View
-import androidx.core.content.ContextCompat
 import androidx.recyclerview.widget.RecyclerView
 import com.nextcloud.talk.R
 import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.databinding.RvItemSearchMessageBinding
 import com.nextcloud.talk.models.domain.SearchMessageEntry
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.DisplayUtils
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
@@ -42,7 +42,8 @@ data class MessageResultItem constructor(
     private val context: Context,
     private val currentUser: User,
     val messageEntry: SearchMessageEntry,
-    private val showHeader: Boolean = false
+    private val showHeader: Boolean = false,
+    private val viewThemeUtils: ViewThemeUtils
 ) :
     AbstractFlexibleItem<MessageResultItem.ViewHolder>(),
     IFilterable<String>,
@@ -77,7 +78,7 @@ data class MessageResultItem constructor(
 
     private fun bindMessageExcerpt(holder: ViewHolder) {
         val messageSpannable = SpannableString(messageEntry.messageExcerpt)
-        val highlightColor = ContextCompat.getColor(context, R.color.colorPrimary)
+        val highlightColor = viewThemeUtils.getElementColor(holder.binding.messageExcerpt.context)
         val highlightedSpan = DisplayUtils.searchAndColor(messageSpannable, messageEntry.searchTerm, highlightColor)
         holder.binding.messageExcerpt.text = highlightedSpan
     }
@@ -104,7 +105,7 @@ data class MessageResultItem constructor(
         const val VIEW_TYPE: Int = R.layout.rv_item_search_message
     }
 
-    override fun getHeader(): GenericTextHeaderItem = MessagesTextHeaderItem(context)
+    override fun getHeader(): GenericTextHeaderItem = MessagesTextHeaderItem(context, viewThemeUtils)
         .apply {
             isHidden = showHeader // FlexibleAdapter needs this hack for some reason
         }

+ 3 - 1
app/src/main/java/com/nextcloud/talk/adapters/items/MessagesTextHeaderItem.kt

@@ -23,8 +23,10 @@ package com.nextcloud.talk.adapters.items
 
 import android.content.Context
 import com.nextcloud.talk.R
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 
-class MessagesTextHeaderItem(context: Context) : GenericTextHeaderItem(context.getString(R.string.messages)) {
+class MessagesTextHeaderItem(context: Context, viewThemeUtils: ViewThemeUtils) :
+    GenericTextHeaderItem(context.getString(R.string.messages), viewThemeUtils) {
     companion object {
         /**
          * "Random" value, just has to be different than other view types

+ 5 - 0
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt

@@ -48,6 +48,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
 import com.nextcloud.talk.databinding.ItemCustomIncomingVoiceMessageBinding
 import com.nextcloud.talk.models.json.chat.ChatMessage
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.preferences.AppPreferences
@@ -66,6 +67,9 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : Message
     @Inject
     var context: Context? = null
 
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
     @JvmField
     @Inject
     var appPreferences: AppPreferences? = null
@@ -93,6 +97,7 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : Message
 
         updateDownloadState(message)
         binding.seekbar.max = message.voiceMessageDuration
+        viewThemeUtils.themeHorizontalSeekBar(binding.seekbar)
 
         if (message.isPlayingVoiceMessage) {
             showPlayButton()

+ 29 - 13
app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt

@@ -25,14 +25,13 @@ package com.nextcloud.talk.adapters.messages
 
 import android.content.Context
 import android.content.Intent
-import android.graphics.PorterDuff
 import android.net.Uri
 import android.text.Spannable
 import android.text.SpannableString
 import android.util.TypedValue
 import android.view.View
-import androidx.core.content.ContextCompat
 import androidx.core.content.res.ResourcesCompat
+import androidx.core.graphics.ColorUtils
 import androidx.core.view.ViewCompat
 import autodagger.AutoInjector
 import coil.load
@@ -44,22 +43,29 @@ import com.nextcloud.talk.databinding.ItemCustomOutcomingTextMessageBinding
 import com.nextcloud.talk.models.json.chat.ChatMessage
 import com.nextcloud.talk.models.json.chat.ReadStatus
 import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
+import com.nextcloud.talk.ui.theme.ServerTheme
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.DisplayUtils.getMessageSelector
 import com.nextcloud.talk.utils.DisplayUtils.searchAndReplaceWithMentionSpan
 import com.nextcloud.talk.utils.TextMatchers
 import com.stfalcon.chatkit.messages.MessageHolders.OutcomingTextMessageViewHolder
-import java.util.HashMap
 import javax.inject.Inject
+import kotlin.math.roundToInt
 
 @AutoInjector(NextcloudTalkApplication::class)
 class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessageViewHolder<ChatMessage>(itemView) {
     private val binding: ItemCustomOutcomingTextMessageBinding = ItemCustomOutcomingTextMessageBinding.bind(itemView)
     private val realView: View = itemView
 
-    @JvmField
     @Inject
-    var context: Context? = null
+    lateinit var context: Context
+
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
+    @Inject
+    lateinit var serverTheme: ServerTheme
 
     lateinit var reactionsInterface: ReactionsInterface
 
@@ -69,7 +75,7 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
         val messageParameters: HashMap<String?, HashMap<String?, String?>>? = message.messageParameters
         var messageString: Spannable = SpannableString(message.text)
         realView.isSelected = false
-        binding.messageTime.setTextColor(context!!.resources.getColor(R.color.white60))
+        binding.messageTime.setTextColor(ColorUtils.setAlphaComponent(serverTheme.colorText, ALPHA_60_INT))
         val layoutParams = binding.messageTime.layoutParams as FlexboxLayout.LayoutParams
         layoutParams.isWrapBefore = false
         var textSize = context!!.resources.getDimension(R.dimen.chat_text_size)
@@ -89,6 +95,8 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
         binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
         binding.messageTime.layoutParams = layoutParams
         binding.messageText.text = messageString
+        binding.messageText.setTextColor(serverTheme.colorText)
+        binding.messageText.setLinkTextColor(serverTheme.colorText)
 
         // parent message handling
         if (!message.isDeleted && message.parentMessage != null) {
@@ -112,8 +120,8 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
 
         readStatusDrawableInt?.let { drawableInt ->
             ResourcesCompat.getDrawable(context!!.resources, drawableInt, null)?.let {
-                it.setColorFilter(ContextCompat.getColor(context!!, R.color.white60), PorterDuff.Mode.SRC_ATOP)
                 binding.checkMark.setImageDrawable(it)
+                viewThemeUtils.colorImageViewText(binding.checkMark)
             }
         }
 
@@ -148,20 +156,25 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
         binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
             ?: context!!.getText(R.string.nc_nick_guest)
         binding.messageQuote.quotedMessage.text = parentChatMessage.text
-        binding.messageQuote.quotedMessage.setTextColor(
-            ContextCompat.getColor(context!!, R.color.nc_outcoming_text_default)
+        binding.messageQuote.quotedMessage.setTextColor(serverTheme.colorText)
+
+        binding.messageQuote.quotedMessageAuthor.setTextColor(
+            ColorUtils.setAlphaComponent(
+                serverTheme.colorText,
+                ALPHA_80_INT
+            )
         )
-        binding.messageQuote.quotedMessageAuthor.setTextColor(ContextCompat.getColor(context!!, R.color.nc_grey))
 
-        binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.white)
+        binding.messageQuote.quoteColoredView.setBackgroundColor(serverTheme.colorText)
     }
 
     private fun setBubbleOnChatMessage(message: ChatMessage) {
         val resources = sharedApplication!!.resources
+        val elementColor = viewThemeUtils.getElementColor(binding.root.context)
         val bgBubbleColor = if (message.isDeleted) {
-            ResourcesCompat.getColor(resources, R.color.bg_message_list_outcoming_bubble_deleted, null)
+            ColorUtils.setAlphaComponent(elementColor, HALF_ALPHA_INT)
         } else {
-            ResourcesCompat.getColor(resources, R.color.bg_message_list_outcoming_bubble, null)
+            elementColor
         }
         if (message.isGrouped) {
             val bubbleDrawable = getMessageSelector(
@@ -221,5 +234,8 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
 
     companion object {
         const val TEXT_SIZE_MULTIPLIER = 2.5
+        private const val HALF_ALPHA_INT: Int = 255 / 2
+        private val ALPHA_60_INT: Int = (255 * 0.6).roundToInt()
+        private val ALPHA_80_INT: Int = (255 * 0.8).roundToInt()
     }
 }

+ 12 - 0
app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java

@@ -29,6 +29,7 @@ package com.nextcloud.talk.adapters.messages;
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.Intent;
+import android.graphics.PorterDuff;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.LayerDrawable;
 import android.net.Uri;
@@ -49,6 +50,7 @@ import com.nextcloud.talk.components.filebrowser.webdav.ReadFilesystemOperation;
 import com.nextcloud.talk.data.user.model.User;
 import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding;
 import com.nextcloud.talk.models.json.chat.ChatMessage;
+import com.nextcloud.talk.ui.theme.ServerTheme;
 import com.nextcloud.talk.utils.DisplayUtils;
 import com.nextcloud.talk.utils.DrawableUtils;
 import com.nextcloud.talk.utils.FileViewerUtils;
@@ -91,6 +93,9 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
     @Inject
     Context context;
 
+    @Inject
+    ServerTheme serverTheme;
+
     @Inject
     OkHttpClient okHttpClient;
 
@@ -175,6 +180,13 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
                 String mimetype = message.getSelectedIndividualHashMap().get(KEY_MIMETYPE);
                 int drawableResourceId = DrawableUtils.INSTANCE.getDrawableResourceIdForMimeType(mimetype);
                 Drawable drawable = ContextCompat.getDrawable(context, drawableResourceId);
+
+                if (drawable != null &&
+                    (drawableResourceId == R.drawable.ic_mimetype_folder ||
+                    drawableResourceId == R.drawable.ic_mimetype_package_x_generic)) {
+                    drawable.setColorFilter(serverTheme.getPrimaryColor(), PorterDuff.Mode.SRC_ATOP);
+                }
+
                 image.getHierarchy().setPlaceholderImage(drawable);
             } else {
                 fetchFileInformation("/" + message.getSelectedIndividualHashMap().get(KEY_PATH),

+ 28 - 11
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt

@@ -25,7 +25,6 @@ package com.nextcloud.talk.adapters.messages
 import android.annotation.SuppressLint
 import android.content.Context
 import android.content.Intent
-import android.graphics.PorterDuff
 import android.net.Uri
 import android.util.Log
 import android.util.TypedValue
@@ -35,6 +34,8 @@ import android.webkit.WebView
 import android.webkit.WebViewClient
 import android.widget.Toast
 import androidx.appcompat.content.res.AppCompatResources
+import androidx.core.content.res.ResourcesCompat
+import androidx.core.graphics.ColorUtils
 import androidx.core.view.ViewCompat
 import autodagger.AutoInjector
 import coil.load
@@ -45,12 +46,15 @@ import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedA
 import com.nextcloud.talk.databinding.ItemCustomOutcomingLocationMessageBinding
 import com.nextcloud.talk.models.json.chat.ChatMessage
 import com.nextcloud.talk.models.json.chat.ReadStatus
+import com.nextcloud.talk.ui.theme.ServerTheme
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.UriUtils
 import com.stfalcon.chatkit.messages.MessageHolders
 import java.net.URLEncoder
 import javax.inject.Inject
+import kotlin.math.roundToInt
 
 @AutoInjector(NextcloudTalkApplication::class)
 class OutcomingLocationMessageViewHolder(incomingView: View) : MessageHolders
@@ -68,6 +72,12 @@ class OutcomingLocationMessageViewHolder(incomingView: View) : MessageHolders
     @Inject
     var context: Context? = null
 
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
+    @Inject
+    lateinit var serverTheme: ServerTheme
+
     lateinit var reactionsInterface: ReactionsInterface
 
     @SuppressLint("SetTextI18n")
@@ -76,7 +86,6 @@ class OutcomingLocationMessageViewHolder(incomingView: View) : MessageHolders
         sharedApplication!!.componentApplication.inject(this)
 
         realView.isSelected = false
-        binding.messageTime.setTextColor(context!!.resources.getColor(R.color.white60))
         val layoutParams = binding.messageTime.layoutParams as FlexboxLayout.LayoutParams
         layoutParams.isWrapBefore = false
 
@@ -85,7 +94,11 @@ class OutcomingLocationMessageViewHolder(incomingView: View) : MessageHolders
         colorizeMessageBubble(message)
         binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
         binding.messageTime.layoutParams = layoutParams
+        binding.messageTime.setTextColor(ColorUtils.setAlphaComponent(serverTheme.colorText, ALPHA_60_INT))
+
         binding.messageText.text = message.text
+        binding.messageText.setTextColor(serverTheme.colorText)
+        binding.messageText.setLinkTextColor(serverTheme.colorText)
 
         // parent message handling
         setParentMessageDataOnMessageItem(message)
@@ -104,8 +117,8 @@ class OutcomingLocationMessageViewHolder(incomingView: View) : MessageHolders
 
         readStatusDrawableInt?.let { drawableInt ->
             AppCompatResources.getDrawable(context!!, drawableInt)?.let {
-                it.setColorFilter(context?.resources!!.getColor(R.color.white60), PorterDuff.Mode.SRC_ATOP)
                 binding.checkMark.setImageDrawable(it)
+                viewThemeUtils.colorImageViewText(binding.checkMark)
             }
         }
 
@@ -200,12 +213,12 @@ class OutcomingLocationMessageViewHolder(incomingView: View) : MessageHolders
             binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
                 ?: context!!.getText(R.string.nc_nick_guest)
             binding.messageQuote.quotedMessage.text = parentChatMessage.text
-            binding.messageQuote.quotedMessage.setTextColor(
-                context!!.resources.getColor(R.color.nc_outcoming_text_default)
+            binding.messageQuote.quotedMessage.setTextColor(serverTheme.colorText)
+            binding.messageQuote.quotedMessageAuthor.setTextColor(
+                ColorUtils.setAlphaComponent(serverTheme.colorText, ALPHA_80_INT)
             )
-            binding.messageQuote.quotedMessageAuthor.setTextColor(context!!.resources.getColor(R.color.nc_grey))
 
-            binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.white)
+            binding.messageQuote.quoteColoredView.setBackgroundColor(serverTheme.colorText)
 
             binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
         } else {
@@ -215,15 +228,16 @@ class OutcomingLocationMessageViewHolder(incomingView: View) : MessageHolders
 
     private fun colorizeMessageBubble(message: ChatMessage) {
         val resources = sharedApplication!!.resources
+        val elementColor = viewThemeUtils.getElementColor(binding.root.context)
         val bgBubbleColor = if (message.isDeleted) {
-            resources.getColor(R.color.bg_message_list_outcoming_bubble_deleted)
+            ColorUtils.setAlphaComponent(elementColor, HALF_ALPHA_INT)
         } else {
-            resources.getColor(R.color.bg_message_list_outcoming_bubble)
+            elementColor
         }
         if (message.isGrouped) {
             val bubbleDrawable = DisplayUtils.getMessageSelector(
                 bgBubbleColor,
-                resources.getColor(R.color.transparent),
+                ResourcesCompat.getColor(resources, R.color.transparent, null),
                 bgBubbleColor,
                 R.drawable.shape_grouped_outcoming_message
             )
@@ -231,7 +245,7 @@ class OutcomingLocationMessageViewHolder(incomingView: View) : MessageHolders
         } else {
             val bubbleDrawable = DisplayUtils.getMessageSelector(
                 bgBubbleColor,
-                resources.getColor(R.color.transparent),
+                ResourcesCompat.getColor(resources, R.color.transparent, null),
                 bgBubbleColor,
                 R.drawable.shape_outcoming_message
             )
@@ -261,5 +275,8 @@ class OutcomingLocationMessageViewHolder(incomingView: View) : MessageHolders
 
     companion object {
         private const val TAG = "LocOutMessageView"
+        private const val HALF_ALPHA_INT: Int = 255 / 2
+        private val ALPHA_60_INT: Int = (255 * 0.6).roundToInt()
+        private val ALPHA_80_INT: Int = (255 * 0.8).roundToInt()
     }
 }

+ 34 - 11
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt

@@ -23,9 +23,11 @@ package com.nextcloud.talk.adapters.messages
 
 import android.annotation.SuppressLint
 import android.content.Context
-import android.graphics.PorterDuff
+import android.content.res.ColorStateList
 import android.view.View
 import androidx.appcompat.content.res.AppCompatResources
+import androidx.core.content.res.ResourcesCompat
+import androidx.core.graphics.ColorUtils
 import androidx.core.view.ViewCompat
 import autodagger.AutoInjector
 import coil.load
@@ -38,11 +40,14 @@ import com.nextcloud.talk.databinding.ItemCustomOutcomingPollMessageBinding
 import com.nextcloud.talk.models.json.chat.ChatMessage
 import com.nextcloud.talk.models.json.chat.ReadStatus
 import com.nextcloud.talk.polls.ui.PollMainDialogFragment
+import com.nextcloud.talk.ui.theme.ServerTheme
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.preferences.AppPreferences
 import com.stfalcon.chatkit.messages.MessageHolders
 import javax.inject.Inject
+import kotlin.math.roundToInt
 
 @AutoInjector(NextcloudTalkApplication::class)
 class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) : MessageHolders
@@ -54,6 +59,12 @@ class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) : Messag
     @Inject
     lateinit var context: Context
 
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
+    @Inject
+    lateinit var serverTheme: ServerTheme
+
     @Inject
     lateinit var appPreferences: AppPreferences
 
@@ -73,7 +84,12 @@ class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) : Messag
         colorizeMessageBubble(message)
 
         itemView.isSelected = false
-        binding.messageTime.setTextColor(context.resources.getColor(R.color.white60))
+        binding.messageTime.setTextColor(
+            ColorUtils.setAlphaComponent(
+                serverTheme.colorText,
+                ALPHA_60_INT
+            )
+        )
 
         // parent message handling
         setParentMessageDataOnMessageItem(message)
@@ -92,8 +108,8 @@ class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) : Messag
 
         readStatusDrawableInt?.let { drawableInt ->
             AppCompatResources.getDrawable(context, drawableInt)?.let {
-                it.setColorFilter(context.resources!!.getColor(R.color.white60), PorterDuff.Mode.SRC_ATOP)
                 binding.checkMark.setImageDrawable(it)
+                viewThemeUtils.colorImageViewText(binding.checkMark)
             }
         }
 
@@ -126,6 +142,9 @@ class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) : Messag
         }
 
         if (pollId != null && pollName != null) {
+            binding.messagePollTitle.setTextColor(serverTheme.colorText)
+            binding.messagePollSubtitle.setTextColor(serverTheme.colorText)
+            binding.messagePollIcon.imageTintList = ColorStateList.valueOf(serverTheme.colorText)
             binding.messagePollTitle.text = pollName
 
             val roomToken = (payload as? MessagePayload)!!.roomToken
@@ -165,12 +184,12 @@ class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) : Messag
             binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
                 ?: context.getText(R.string.nc_nick_guest)
             binding.messageQuote.quotedMessage.text = parentChatMessage.text
-            binding.messageQuote.quotedMessage.setTextColor(
-                context.resources.getColor(R.color.nc_outcoming_text_default)
+            binding.messageQuote.quotedMessage.setTextColor(serverTheme.colorText)
+            binding.messageQuote.quotedMessageAuthor.setTextColor(
+                ColorUtils.setAlphaComponent(serverTheme.colorText, ALPHA_80_INT)
             )
-            binding.messageQuote.quotedMessageAuthor.setTextColor(context.resources.getColor(R.color.nc_grey))
 
-            binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.white)
+            binding.messageQuote.quoteColoredView.setBackgroundColor(serverTheme.colorText)
 
             binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
         } else {
@@ -180,15 +199,16 @@ class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) : Messag
 
     private fun colorizeMessageBubble(message: ChatMessage) {
         val resources = sharedApplication!!.resources
+        val elementColor = viewThemeUtils.getElementColor(binding.root.context)
         val bgBubbleColor = if (message.isDeleted) {
-            resources.getColor(R.color.bg_message_list_outcoming_bubble_deleted)
+            ColorUtils.setAlphaComponent(elementColor, HALF_ALPHA_INT)
         } else {
-            resources.getColor(R.color.bg_message_list_outcoming_bubble)
+            elementColor
         }
         if (message.isGrouped) {
             val bubbleDrawable = DisplayUtils.getMessageSelector(
                 bgBubbleColor,
-                resources.getColor(R.color.transparent),
+                ResourcesCompat.getColor(resources, R.color.transparent, null),
                 bgBubbleColor,
                 R.drawable.shape_grouped_outcoming_message
             )
@@ -196,7 +216,7 @@ class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) : Messag
         } else {
             val bubbleDrawable = DisplayUtils.getMessageSelector(
                 bgBubbleColor,
-                resources.getColor(R.color.transparent),
+                ResourcesCompat.getColor(resources, R.color.transparent, null),
                 bgBubbleColor,
                 R.drawable.shape_outcoming_message
             )
@@ -210,5 +230,8 @@ class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) : Messag
 
     companion object {
         private val TAG = NextcloudTalkApplication::class.java.simpleName
+        private val ALPHA_60_INT: Int = (255 * 0.6).roundToInt()
+        private val ALPHA_80_INT: Int = (255 * 0.8).roundToInt()
+        private const val HALF_ALPHA_INT: Int = 255 / 2
     }
 }

+ 37 - 10
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt

@@ -31,6 +31,8 @@ import android.view.View
 import android.widget.SeekBar
 import androidx.appcompat.content.res.AppCompatResources
 import androidx.core.content.ContextCompat
+import androidx.core.content.res.ResourcesCompat
+import androidx.core.graphics.ColorUtils
 import androidx.core.view.ViewCompat
 import androidx.work.WorkInfo
 import androidx.work.WorkManager
@@ -42,12 +44,15 @@ import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedA
 import com.nextcloud.talk.databinding.ItemCustomOutcomingVoiceMessageBinding
 import com.nextcloud.talk.models.json.chat.ChatMessage
 import com.nextcloud.talk.models.json.chat.ReadStatus
+import com.nextcloud.talk.ui.theme.ServerTheme
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.preferences.AppPreferences
 import com.stfalcon.chatkit.messages.MessageHolders
 import java.util.concurrent.ExecutionException
 import javax.inject.Inject
+import kotlin.math.roundToInt
 
 @AutoInjector(NextcloudTalkApplication::class)
 class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
@@ -60,6 +65,12 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
     @Inject
     var context: Context? = null
 
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
+    @Inject
+    lateinit var serverTheme: ServerTheme
+
     @JvmField
     @Inject
     var appPreferences: AppPreferences? = null
@@ -80,13 +91,19 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
         colorizeMessageBubble(message)
 
         itemView.isSelected = false
-        binding.messageTime.setTextColor(context!!.resources.getColor(R.color.white60))
+        binding.messageTime.setTextColor(
+            ColorUtils.setAlphaComponent(
+                serverTheme.colorText,
+                ALPHA_60_INT
+            )
+        )
 
         // parent message handling
         setParentMessageDataOnMessageItem(message)
 
         updateDownloadState(message)
         binding.seekbar.max = message.voiceMessageDuration
+        viewThemeUtils.themeHorizontalSeekBar(binding.seekbar, serverTheme.colorText)
 
         handleIsPlayingVoiceMessageState(message)
 
@@ -124,8 +141,8 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
 
         readStatusDrawableInt?.let { drawableInt ->
             AppCompatResources.getDrawable(context!!, drawableInt)?.let {
-                it.setColorFilter(context?.resources!!.getColor(R.color.white60), PorterDuff.Mode.SRC_ATOP)
                 binding.checkMark.setImageDrawable(it)
+                viewThemeUtils.colorImageViewText(binding.checkMark)
             }
         }
 
@@ -148,6 +165,7 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
                 context!!,
                 R.drawable.ic_baseline_play_arrow_voice_message_24
             )
+            binding.playPauseBtn.icon.setColorFilter(serverTheme.colorText, PorterDuff.Mode.SRC_ATOP)
             binding.seekbar.progress = SEEKBAR_START
             message.resetVoiceMessage = false
         }
@@ -168,6 +186,7 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
                 context!!,
                 R.drawable.ic_baseline_pause_voice_message_24
             )
+            binding.playPauseBtn.icon.setColorFilter(serverTheme.colorText, PorterDuff.Mode.SRC_ATOP)
             binding.seekbar.progress = message.voiceMessagePlayedSeconds
         } else {
             binding.playPauseBtn.visibility = View.VISIBLE
@@ -175,6 +194,7 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
                 context!!,
                 R.drawable.ic_baseline_play_arrow_voice_message_24
             )
+            binding.playPauseBtn.icon.setColorFilter(serverTheme.colorText, PorterDuff.Mode.SRC_ATOP)
         }
     }
 
@@ -250,12 +270,15 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
             binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
                 ?: context!!.getText(R.string.nc_nick_guest)
             binding.messageQuote.quotedMessage.text = parentChatMessage.text
-            binding.messageQuote.quotedMessage.setTextColor(
-                context!!.resources.getColor(R.color.nc_outcoming_text_default)
+            binding.messageQuote.quotedMessage.setTextColor(serverTheme.colorText)
+            binding.messageQuote.quotedMessageAuthor.setTextColor(
+                ColorUtils.setAlphaComponent(
+                    serverTheme.colorText,
+                    ALPHA_80_INT
+                )
             )
-            binding.messageQuote.quotedMessageAuthor.setTextColor(context!!.resources.getColor(R.color.nc_grey))
 
-            binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.white)
+            binding.messageQuote.quoteColoredView.setBackgroundColor(serverTheme.colorText)
 
             binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
         } else {
@@ -265,15 +288,16 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
 
     private fun colorizeMessageBubble(message: ChatMessage) {
         val resources = sharedApplication!!.resources
+        val elementColor = viewThemeUtils.getElementColor(binding.root.context)
         val bgBubbleColor = if (message.isDeleted) {
-            resources.getColor(R.color.bg_message_list_outcoming_bubble_deleted)
+            ColorUtils.setAlphaComponent(elementColor, HALF_ALPHA_INT)
         } else {
-            resources.getColor(R.color.bg_message_list_outcoming_bubble)
+            elementColor
         }
         if (message.isGrouped) {
             val bubbleDrawable = DisplayUtils.getMessageSelector(
                 bgBubbleColor,
-                resources.getColor(R.color.transparent),
+                ResourcesCompat.getColor(resources, R.color.transparent, null),
                 bgBubbleColor,
                 R.drawable.shape_grouped_outcoming_message
             )
@@ -281,7 +305,7 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
         } else {
             val bubbleDrawable = DisplayUtils.getMessageSelector(
                 bgBubbleColor,
-                resources.getColor(R.color.transparent),
+                ResourcesCompat.getColor(resources, R.color.transparent, null),
                 bgBubbleColor,
                 R.drawable.shape_outcoming_message
             )
@@ -300,5 +324,8 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
     companion object {
         private const val TAG = "VoiceOutMessageView"
         private const val SEEKBAR_START: Int = 0
+        private const val HALF_ALPHA_INT: Int = 255 / 2
+        private val ALPHA_80_INT: Int = (255 * 0.8).roundToInt()
+        private val ALPHA_60_INT: Int = (255 * 0.6).roundToInt()
     }
 }

+ 4 - 1
app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt

@@ -61,6 +61,7 @@ import com.nextcloud.talk.dagger.modules.ViewModelModule
 import com.nextcloud.talk.jobs.AccountRemovalWorker
 import com.nextcloud.talk.jobs.CapabilitiesWorker
 import com.nextcloud.talk.jobs.SignalingSettingsWorker
+import com.nextcloud.talk.ui.theme.ThemeModule
 import com.nextcloud.talk.utils.ClosedInterfaceImpl
 import com.nextcloud.talk.utils.DeviceUtils
 import com.nextcloud.talk.utils.DisplayUtils
@@ -96,7 +97,8 @@ import javax.inject.Singleton
         ArbitraryStorageModule::class,
         ViewModelModule::class,
         RepositoryModule::class,
-        UtilsModule::class
+        UtilsModule::class,
+        ThemeModule::class
     ]
 )
 @Singleton
@@ -120,6 +122,7 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {
         override fun preKey(database: SQLiteDatabase) {
             // unused atm
         }
+
         override fun postKey(database: SQLiteDatabase) {
             Log.i("TalkApplication", "DB cipher_migrate START")
             database.rawExecSQL("PRAGMA cipher_migrate;")

+ 12 - 1
app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt

@@ -154,6 +154,8 @@ import com.nextcloud.talk.ui.dialog.MessageActionsDialog
 import com.nextcloud.talk.ui.dialog.ShowReactionsDialog
 import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions
 import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
+import com.nextcloud.talk.ui.theme.ServerTheme
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.AttendeePermissionsUtil
 import com.nextcloud.talk.utils.ConductorRemapping
@@ -235,6 +237,12 @@ class ChatController(args: Bundle) :
     @Inject
     lateinit var permissionUtil: PlatformPermissionUtil
 
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
+    @Inject
+    lateinit var serverTheme: ServerTheme
+
     val disposables = DisposableSet()
 
     var roomToken: String? = null
@@ -872,6 +880,8 @@ class ChatController(args: Bundle) :
                 .nc_description_send_message_button
         )
 
+        viewThemeUtils.colorImageView(binding.messageInputView.button)
+
         if (currentConversation != null && currentConversation?.roomId != null) {
             loadAvatarForStatusBar()
             setTitle()
@@ -2675,7 +2685,8 @@ class ChatController(args: Bundle) :
                 chatMessage,
                 conversationUser,
                 hasChatPermission,
-                ncApi!!
+                ncApi!!,
+                serverTheme
             ).show()
         }
     }

+ 11 - 11
app/src/main/java/com/nextcloud/talk/controllers/ContactsController.kt

@@ -65,6 +65,7 @@ import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter
 import com.nextcloud.talk.models.json.participants.Participant
 import com.nextcloud.talk.ui.dialog.ContactsBottomDialog
 import com.nextcloud.talk.users.UserManager
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.ConductorRemapping
 import com.nextcloud.talk.utils.bundle.BundleKeys
@@ -103,6 +104,9 @@ class ContactsController(args: Bundle) :
     @Inject
     lateinit var ncApi: NcApi
 
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
     private var credentials: String? = null
     private var currentUser: User? = null
     private var contactsQueryDisposable: Disposable? = null
@@ -492,13 +496,14 @@ class ContactsController(args: Bundle) :
                 val headerTitle = getHeaderTitle(participant)
                 var genericTextHeaderItem: GenericTextHeaderItem
                 if (!userHeaderItems.containsKey(headerTitle)) {
-                    genericTextHeaderItem = GenericTextHeaderItem(headerTitle)
+                    genericTextHeaderItem = GenericTextHeaderItem(headerTitle, viewThemeUtils)
                     userHeaderItems.put(headerTitle, genericTextHeaderItem)
                 }
                 val newContactItem = ContactItem(
                     participant,
                     currentUser,
-                    userHeaderItems[headerTitle]
+                    userHeaderItems[headerTitle],
+                    viewThemeUtils
                 )
                 if (!contactItems!!.contains(newContactItem)) {
                     newUserItemList.add(newContactItem)
@@ -618,21 +623,16 @@ class ContactsController(args: Bundle) :
         binding.controllerGenericRv.recyclerView.setHasFixedSize(true)
         binding.controllerGenericRv.recyclerView.adapter = adapter
         binding.controllerGenericRv.swipeRefreshLayout.setOnRefreshListener { fetchData() }
-        binding.controllerGenericRv.swipeRefreshLayout.setColorSchemeResources(R.color.colorPrimary)
-        binding.controllerGenericRv.swipeRefreshLayout
-            .setProgressBackgroundColorSchemeResource(R.color.refresh_spinner_background)
+
+        viewThemeUtils.themeSwipeRefreshLayout(binding.controllerGenericRv.swipeRefreshLayout)
+
         binding.joinConversationViaLink.joinConversationViaLinkImageView
             .background
             .setColorFilter(
                 ResourcesCompat.getColor(resources!!, R.color.colorBackgroundDarker, null),
                 PorterDuff.Mode.SRC_IN
             )
-        binding.conversationPrivacyToggle.publicCallLink
-            .background
-            .setColorFilter(
-                ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null),
-                PorterDuff.Mode.SRC_IN
-            )
+        viewThemeUtils.colorImageViewButton(binding.conversationPrivacyToggle.publicCallLink)
         disengageProgressBar()
     }
 

+ 63 - 31
app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt

@@ -73,6 +73,7 @@ import com.nextcloud.talk.models.json.participants.Participant.ActorType.GROUPS
 import com.nextcloud.talk.models.json.participants.Participant.ActorType.USERS
 import com.nextcloud.talk.models.json.participants.ParticipantsOverall
 import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.DateConstants
 import com.nextcloud.talk.utils.DateUtils
@@ -112,6 +113,9 @@ class ConversationInfoController(args: Bundle) :
     @Inject
     lateinit var eventBus: EventBus
 
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
     private val conversationToken: String?
     private val conversationUser: User?
     private val hasAvatarSpacing: Boolean
@@ -181,6 +185,34 @@ class ConversationInfoController(args: Bundle) :
         }
 
         fetchRoomInfo()
+
+        themeCategories()
+        themeSwitchPreferences()
+    }
+
+    private fun themeSwitchPreferences() {
+        binding.run {
+            listOf(
+                binding.webinarInfoView.conversationInfoLobby,
+                binding.notificationSettingsView.callNotifications,
+                binding.notificationSettingsView.conversationInfoPriorityConversation
+            ).forEach(viewThemeUtils::colorSwitchPreference)
+        }
+    }
+
+    private fun themeCategories() {
+        binding.run {
+            listOf(
+                conversationInfoName,
+                conversationDescription,
+                otherRoomOptions,
+                participantsListCategory,
+                ownOptions,
+                categorySharedItems,
+                binding.webinarInfoView.conversationInfoWebinar,
+                binding.notificationSettingsView.notificationSettingsCategory
+            ).forEach(viewThemeUtils::colorPreferenceCategory)
+        }
     }
 
     private fun showSharedItems() {
@@ -299,7 +331,7 @@ class ConversationInfoController(args: Bundle) :
 
         val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
 
-        ncApi?.setLobbyForConversation(
+        ncApi.setLobbyForConversation(
             ApiUtils.getCredentials(conversationUser!!.username, conversationUser.token),
             ApiUtils.getUrlForRoomWebinaryLobby(apiVersion, conversationUser.baseUrl, conversation!!.token),
             state,
@@ -343,7 +375,7 @@ class ConversationInfoController(args: Bundle) :
 
     override fun onDetach(view: View) {
         super.onDetach(view)
-        eventBus?.unregister(this)
+        eventBus.unregister(this)
     }
 
     private fun showDeleteConversationDialog(savedInstanceState: Bundle?) {
@@ -352,11 +384,11 @@ class ConversationInfoController(args: Bundle) :
                 .setTopColorRes(R.color.nc_darkRed)
                 .setIcon(
                     DisplayUtils.getTintedDrawable(
-                        context!!.resources,
+                        context.resources,
                         R.drawable.ic_delete_black_24dp, R.color.bg_default
                     )
                 )
-                .setPositiveButtonColor(context!!.resources.getColor(R.color.nc_darkRed))
+                .setPositiveButtonColor(context.resources.getColor(R.color.nc_darkRed))
                 .setTitle(R.string.nc_delete_call)
                 .setMessage(R.string.nc_delete_conversation_more)
                 .setPositiveButton(R.string.nc_delete) { deleteConversation() }
@@ -409,7 +441,7 @@ class ConversationInfoController(args: Bundle) :
             if (participant.sessionId != null) {
                 userItem.isOnline = !participant.sessionId.equals("0")
             } else {
-                userItem.isOnline = !participant.sessionIds!!.isEmpty()
+                userItem.isOnline = !participant.sessionIds.isEmpty()
             }
 
             if (participant.calculatedActorType == USERS &&
@@ -453,7 +485,7 @@ class ConversationInfoController(args: Bundle) :
         val fieldMap = HashMap<String, Boolean>()
         fieldMap["includeStatus"] = true
 
-        ncApi?.getPeersForCall(
+        ncApi.getPeersForCall(
             credentials,
             ApiUtils.getUrlForParticipants(
                 apiVersion,
@@ -504,7 +536,7 @@ class ConversationInfoController(args: Bundle) :
         bundle.putStringArrayList(BundleKeys.KEY_EXISTING_PARTICIPANTS, existingParticipantsId)
         bundle.putString(BundleKeys.KEY_TOKEN, conversation!!.token)
 
-        getRouter().pushController(
+        router.pushController(
             (
                 RouterTransaction.with(
                     ContactsController(bundle)
@@ -537,11 +569,11 @@ class ConversationInfoController(args: Bundle) :
                 .setTopColorRes(R.color.nc_darkRed)
                 .setIcon(
                     DisplayUtils.getTintedDrawable(
-                        context!!.resources,
+                        context.resources,
                         R.drawable.ic_delete_black_24dp, R.color.bg_default
                     )
                 )
-                .setPositiveButtonColor(context!!.resources.getColor(R.color.nc_darkRed))
+                .setPositiveButtonColor(context.resources.getColor(R.color.nc_darkRed))
                 .setTitle(R.string.nc_clear_history)
                 .setMessage(R.string.nc_clear_history_warning)
                 .setPositiveButton(R.string.nc_delete_all) { clearHistory() }
@@ -555,7 +587,7 @@ class ConversationInfoController(args: Bundle) :
     private fun clearHistory() {
         val apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
 
-        ncApi?.clearChatHistory(
+        ncApi.clearChatHistory(
             credentials,
             ApiUtils.getUrlForChat(apiVersion, conversationUser!!.baseUrl, conversationToken)
         )
@@ -567,7 +599,7 @@ class ConversationInfoController(args: Bundle) :
                 }
 
                 override fun onNext(genericOverall: GenericOverall) {
-                    Toast.makeText(context, context?.getString(R.string.nc_clear_history_success), Toast.LENGTH_LONG)
+                    Toast.makeText(context, context.getString(R.string.nc_clear_history_success), Toast.LENGTH_LONG)
                         .show()
                 }
 
@@ -606,7 +638,7 @@ class ConversationInfoController(args: Bundle) :
             apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
         }
 
-        ncApi?.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, conversationUser!!.baseUrl, conversationToken))
+        ncApi.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, conversationUser!!.baseUrl, conversationToken))
             ?.subscribeOn(Schedulers.io())
             ?.observeOn(AndroidSchedulers.mainThread())
             ?.subscribe(object : Observer<RoomOverall> {
@@ -765,8 +797,8 @@ class ConversationInfoController(args: Bundle) :
             )
             Conversation.ConversationType.ROOM_SYSTEM -> {
                 val layers = arrayOfNulls<Drawable>(2)
-                layers[0] = ContextCompat.getDrawable(context!!, R.drawable.ic_launcher_background)
-                layers[1] = ContextCompat.getDrawable(context!!, R.drawable.ic_launcher_foreground)
+                layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background)
+                layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground)
                 val layerDrawable = LayerDrawable(layers)
                 binding.avatarImage.hierarchy.setPlaceholderImage(DisplayUtils.getRoundedDrawable(layerDrawable))
             }
@@ -800,7 +832,7 @@ class ConversationInfoController(args: Bundle) :
         if (participant.type == Participant.ParticipantType.MODERATOR ||
             participant.type == Participant.ParticipantType.GUEST_MODERATOR
         ) {
-            ncApi?.demoteAttendeeFromModerator(
+            ncApi.demoteAttendeeFromModerator(
                 credentials,
                 ApiUtils.getUrlForRoomModerators(
                     apiVersion,
@@ -815,7 +847,7 @@ class ConversationInfoController(args: Bundle) :
         } else if (participant.type == Participant.ParticipantType.USER ||
             participant.type == Participant.ParticipantType.GUEST
         ) {
-            ncApi?.promoteAttendeeToModerator(
+            ncApi.promoteAttendeeToModerator(
                 credentials,
                 ApiUtils.getUrlForRoomModerators(
                     apiVersion,
@@ -851,7 +883,7 @@ class ConversationInfoController(args: Bundle) :
         }
 
         if (participant.type == Participant.ParticipantType.MODERATOR) {
-            ncApi?.demoteModeratorToUser(
+            ncApi.demoteModeratorToUser(
                 credentials,
                 ApiUtils.getUrlForRoomModerators(
                     apiVersion,
@@ -864,7 +896,7 @@ class ConversationInfoController(args: Bundle) :
                 ?.observeOn(AndroidSchedulers.mainThread())
                 ?.subscribe(subscriber)
         } else if (participant.type == Participant.ParticipantType.USER) {
-            ncApi?.promoteUserToModerator(
+            ncApi.promoteUserToModerator(
                 credentials,
                 ApiUtils.getUrlForRoomModerators(
                     apiVersion,
@@ -881,7 +913,7 @@ class ConversationInfoController(args: Bundle) :
 
     fun removeAttendeeFromConversation(apiVersion: Int, participant: Participant) {
         if (apiVersion >= ApiUtils.APIv4) {
-            ncApi?.removeAttendeeFromConversation(
+            ncApi.removeAttendeeFromConversation(
                 credentials,
                 ApiUtils.getUrlForAttendees(
                     apiVersion,
@@ -914,7 +946,7 @@ class ConversationInfoController(args: Bundle) :
             if (participant.type == Participant.ParticipantType.GUEST ||
                 participant.type == Participant.ParticipantType.USER_FOLLOWING_LINK
             ) {
-                ncApi?.removeParticipantFromConversation(
+                ncApi.removeParticipantFromConversation(
                     credentials,
                     ApiUtils.getUrlForRemovingParticipantFromConversation(
                         conversationUser!!.baseUrl,
@@ -944,7 +976,7 @@ class ConversationInfoController(args: Bundle) :
                         }
                     })
             } else {
-                ncApi?.removeParticipantFromConversation(
+                ncApi.removeParticipantFromConversation(
                     credentials,
                     ApiUtils.getUrlForRemovingParticipantFromConversation(
                         conversationUser!!.baseUrl,
@@ -987,12 +1019,12 @@ class ConversationInfoController(args: Bundle) :
 
         val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
 
-        if (participant.calculatedActorType == USERS && participant.calculatedActorId == conversationUser!!.userId) {
+        if (participant.calculatedActorType == USERS && participant.calculatedActorId == conversationUser.userId) {
             if (participant.attendeePin?.isNotEmpty() == true) {
                 val items = mutableListOf(
                     BasicListItemWithImage(
                         R.drawable.ic_lock_grey600_24px,
-                        context!!.getString(R.string.nc_attendee_pin, participant.attendeePin)
+                        context.getString(R.string.nc_attendee_pin, participant.attendeePin)
                     )
                 )
                 MaterialDialog(activity!!, BottomSheet(WRAP_CONTENT)).show {
@@ -1018,7 +1050,7 @@ class ConversationInfoController(args: Bundle) :
             val items = mutableListOf(
                 BasicListItemWithImage(
                     R.drawable.ic_delete_grey600_24dp,
-                    context!!.getString(R.string.nc_remove_group_and_members)
+                    context.getString(R.string.nc_remove_group_and_members)
                 )
             )
             MaterialDialog(activity!!, BottomSheet(WRAP_CONTENT)).show {
@@ -1038,7 +1070,7 @@ class ConversationInfoController(args: Bundle) :
             val items = mutableListOf(
                 BasicListItemWithImage(
                     R.drawable.ic_delete_grey600_24dp,
-                    context!!.getString(R.string.nc_remove_circle_and_members)
+                    context.getString(R.string.nc_remove_circle_and_members)
                 )
             )
             MaterialDialog(activity!!, BottomSheet(WRAP_CONTENT)).show {
@@ -1057,19 +1089,19 @@ class ConversationInfoController(args: Bundle) :
         val items = mutableListOf(
             BasicListItemWithImage(
                 R.drawable.ic_lock_grey600_24px,
-                context!!.getString(R.string.nc_attendee_pin, participant.attendeePin)
+                context.getString(R.string.nc_attendee_pin, participant.attendeePin)
             ),
             BasicListItemWithImage(
                 R.drawable.ic_pencil_grey600_24dp,
-                context!!.getString(R.string.nc_promote)
+                context.getString(R.string.nc_promote)
             ),
             BasicListItemWithImage(
                 R.drawable.ic_pencil_grey600_24dp,
-                context!!.getString(R.string.nc_demote)
+                context.getString(R.string.nc_demote)
             ),
             BasicListItemWithImage(
                 R.drawable.ic_delete_grey600_24dp,
-                context!!.getString(R.string.nc_remove_participant)
+                context.getString(R.string.nc_remove_participant)
             )
         )
 
@@ -1167,8 +1199,8 @@ class ConversationInfoController(args: Bundle) :
                 return 1
             }
 
-            return left.model.displayName!!.toLowerCase(Locale.ROOT).compareTo(
-                right.model.displayName!!.toLowerCase(Locale.ROOT)
+            return left.model.displayName!!.lowercase(Locale.ROOT).compareTo(
+                right.model.displayName!!.lowercase(Locale.ROOT)
             )
         }
     }

+ 16 - 8
app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java

@@ -92,6 +92,7 @@ import com.nextcloud.talk.models.json.statuses.StatusesOverall;
 import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository;
 import com.nextcloud.talk.ui.dialog.ChooseAccountDialogFragment;
 import com.nextcloud.talk.ui.dialog.ConversationsListBottomDialog;
+import com.nextcloud.talk.ui.theme.ViewThemeUtils;
 import com.nextcloud.talk.users.UserManager;
 import com.nextcloud.talk.utils.ApiUtils;
 import com.nextcloud.talk.utils.AttendeePermissionsUtil;
@@ -181,6 +182,9 @@ public class ConversationsListController extends BaseController implements Flexi
     @Inject
     UnifiedSearchRepository unifiedSearchRepository;
 
+    @Inject
+    ViewThemeUtils viewThemeUtils;
+
     @BindView(R.id.recycler_view)
     RecyclerView recyclerView;
 
@@ -618,7 +622,7 @@ public class ConversationsListController extends BaseController implements Flexi
 
                     GenericTextHeaderItem genericTextHeaderItem;
                     if (!callHeaderItems.containsKey(headerTitle)) {
-                        genericTextHeaderItem = new GenericTextHeaderItem(headerTitle);
+                        genericTextHeaderItem = new GenericTextHeaderItem(headerTitle, viewThemeUtils);
                         callHeaderItems.put(headerTitle, genericTextHeaderItem);
                     }
 
@@ -627,7 +631,8 @@ public class ConversationsListController extends BaseController implements Flexi
                             conversation,
                             currentUser,
                             getActivity(),
-                            userStatuses.get(conversation.getName()));
+                            userStatuses.get(conversation.getName()),
+                            viewThemeUtils);
                         conversationItems.add(conversationItem);
 
                         ConversationItem conversationItemWithHeader = new ConversationItem(
@@ -635,7 +640,8 @@ public class ConversationsListController extends BaseController implements Flexi
                             currentUser,
                             getActivity(),
                             callHeaderItems.get(headerTitle),
-                            userStatuses.get(conversation.getName()));
+                            userStatuses.get(conversation.getName()),
+                            viewThemeUtils);
                         conversationItemsWithHeader.add(conversationItemWithHeader);
                     }
                 }
@@ -699,7 +705,7 @@ public class ConversationsListController extends BaseController implements Flexi
 
                         GenericTextHeaderItem genericTextHeaderItem;
                         if (!callHeaderItems.containsKey(headerTitle)) {
-                            genericTextHeaderItem = new GenericTextHeaderItem(headerTitle);
+                            genericTextHeaderItem = new GenericTextHeaderItem(headerTitle, viewThemeUtils);
                             callHeaderItems.put(headerTitle, genericTextHeaderItem);
                         }
 
@@ -708,7 +714,8 @@ public class ConversationsListController extends BaseController implements Flexi
                             currentUser,
                             getActivity(),
                             callHeaderItems.get(headerTitle),
-                            userStatuses.get(conversation.getName()));
+                            userStatuses.get(conversation.getName()),
+                            viewThemeUtils);
 
                         openConversationItems.add(conversationItem);
                     }
@@ -776,8 +783,7 @@ public class ConversationsListController extends BaseController implements Flexi
         });
 
         swipeRefreshLayout.setOnRefreshListener(() -> fetchData());
-        swipeRefreshLayout.setColorSchemeResources(R.color.colorPrimary);
-        swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.refresh_spinner_background);
+        viewThemeUtils.themeSwipeRefreshLayout(swipeRefreshLayout);
 
         emptyLayoutView.setOnClickListener(v -> showNewConversationsScreen());
         floatingActionButton.setOnClickListener(v -> {
@@ -785,6 +791,8 @@ public class ConversationsListController extends BaseController implements Flexi
             showNewConversationsScreen();
         });
 
+        viewThemeUtils.themeFAB(floatingActionButton);
+
         if (getActivity() != null && getActivity() instanceof MainActivity) {
             MainActivity activity = (MainActivity) getActivity();
 
@@ -1409,7 +1417,7 @@ public class ConversationsListController extends BaseController implements Flexi
                 List<AbstractFlexibleItem> adapterItems = new ArrayList<>(entries.size() + 1);
                 for (int i = 0; i < entries.size(); i++) {
                     final boolean showHeader = i == 0;
-                    adapterItems.add(new MessageResultItem(context, currentUser, entries.get(i), showHeader));
+                    adapterItems.add(new MessageResultItem(context, currentUser, entries.get(i), showHeader, viewThemeUtils));
                 }
                 if (results.getHasMore()) {
                     adapterItems.add(LoadMoreResultsItem.INSTANCE);

+ 36 - 32
app/src/main/java/com/nextcloud/talk/controllers/ProfileController.kt

@@ -24,10 +24,8 @@ package com.nextcloud.talk.controllers
 import android.app.Activity
 import android.content.Intent
 import android.content.pm.PackageManager
-import android.content.res.ColorStateList
 import android.graphics.Bitmap
 import android.graphics.BitmapFactory
-import android.graphics.Color
 import android.net.Uri
 import android.os.Bundle
 import android.text.Editable
@@ -43,7 +41,6 @@ import android.view.ViewGroup
 import android.widget.Toast
 import androidx.annotation.ColorInt
 import androidx.annotation.DrawableRes
-import androidx.core.content.ContextCompat
 import androidx.core.graphics.drawable.DrawableCompat
 import androidx.core.view.ViewCompat
 import androidx.recyclerview.widget.RecyclerView
@@ -69,6 +66,7 @@ import com.nextcloud.talk.models.json.userprofile.UserProfileFieldsOverall
 import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
 import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
 import com.nextcloud.talk.ui.dialog.ScopeDialog
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.users.UserManager
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.DisplayUtils
@@ -110,6 +108,9 @@ class ProfileController : NewBaseController(R.layout.controller_profile) {
     @Inject
     lateinit var permissionUtil: PlatformPermissionUtil
 
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
     private var currentUser: User? = null
     private var edit = false
     private var adapter: UserInfoAdapter? = null
@@ -196,7 +197,7 @@ class ProfileController : NewBaseController(R.layout.controller_profile) {
 
     override fun onAttach(view: View) {
         super.onAttach(view)
-        adapter = UserInfoAdapter(null, activity!!.resources.getColor(R.color.colorPrimary), this)
+        adapter = UserInfoAdapter(null, viewThemeUtils.getElementColor(activity!!), this)
         binding.userinfoList.adapter = adapter
         binding.userinfoList.setItemViewCacheSize(DEFAULT_CACHE_SIZE)
         currentUser = userManager.currentUser.blockingGet()
@@ -260,6 +261,13 @@ class ProfileController : NewBaseController(R.layout.controller_profile) {
                     // unused atm
                 }
             })
+
+        colorIcons()
+    }
+
+    private fun colorIcons() {
+        viewThemeUtils.colorImageView(binding.avatarChoose)
+        viewThemeUtils.colorImageView(binding.avatarCamera)
     }
 
     private fun isAllEmpty(items: Array<String?>): Boolean {
@@ -301,7 +309,8 @@ class ProfileController : NewBaseController(R.layout.controller_profile) {
             binding.emptyList.root.visibility = View.VISIBLE
             setErrorMessageForMultiList(
                 activity!!.getString(R.string.userinfo_no_info_headline),
-                activity!!.getString(R.string.userinfo_no_info_text), R.drawable.ic_user
+                activity!!.getString(R.string.userinfo_no_info_text),
+                R.drawable.ic_user
             )
         } else {
             binding.emptyList.root.visibility = View.GONE
@@ -616,11 +625,13 @@ class ProfileController : NewBaseController(R.layout.controller_profile) {
         val builder = MultipartBody.Builder()
         builder.setType(MultipartBody.FORM)
         builder.addFormDataPart(
-            "files[]", file!!.name,
+            "files[]",
+            file!!.name,
             file.asRequestBody(IMAGE_PREFIX_GENERIC.toMediaTypeOrNull())
         )
         val filePart: MultipartBody.Part = MultipartBody.Part.createFormData(
-            "files[]", file.name,
+            "files[]",
+            file.name,
             file.asRequestBody(IMAGE_JPG.toMediaTypeOrNull())
         )
 
@@ -643,7 +654,8 @@ class ProfileController : NewBaseController(R.layout.controller_profile) {
 
                 override fun onError(e: Throwable) {
                     Toast.makeText(
-                        applicationContext, context.getString(R.string.default_error_msg),
+                        applicationContext,
+                        context.getString(R.string.default_error_msg),
                         Toast
                             .LENGTH_LONG
                     ).show()
@@ -688,7 +700,8 @@ class ProfileController : NewBaseController(R.layout.controller_profile) {
     }
 
     class UserInfoDetailsItem(
-        @field:DrawableRes @param:DrawableRes var icon: Int,
+        @field:DrawableRes @param:DrawableRes
+        var icon: Int,
         var text: String?,
         var hint: String,
         val field: Field,
@@ -748,22 +761,14 @@ class ProfileController : NewBaseController(R.layout.controller_profile) {
             DrawableCompat.setTint(holder.binding.icon.drawable, mTintColor)
             if (!TextUtils.isEmpty(item.text) || controller.edit) {
                 holder.binding.userInfoDetailContainer.visibility = View.VISIBLE
-                if (controller.activity != null) {
-                    holder.binding.userInfoEditText.setTextColor(
-                        ContextCompat.getColor(
-                            controller.activity!!,
-                            R.color.conversation_item_header
-                        )
-                    )
-                }
+                controller.viewThemeUtils.colorTextInputLayout(holder.binding.userInfoInputLayout)
                 if (controller.edit &&
                     controller.editableFields.contains(item.field.toString().lowercase())
                 ) {
-                    holder.binding.userInfoEditText.isEnabled = true
-                    holder.binding.userInfoEditText.isFocusableInTouchMode = true
-                    holder.binding.userInfoEditText.isEnabled = true
-                    holder.binding.userInfoEditText.isCursorVisible = true
-                    holder.binding.userInfoEditText.backgroundTintList = ColorStateList.valueOf(mTintColor)
+                    holder.binding.userInfoEditTextEdit.isEnabled = true
+                    holder.binding.userInfoEditTextEdit.isFocusableInTouchMode = true
+                    holder.binding.userInfoEditTextEdit.isEnabled = true
+                    holder.binding.userInfoEditTextEdit.isCursorVisible = true
                     holder.binding.scope.setOnClickListener {
                         ScopeDialog(
                             controller.activity!!,
@@ -774,11 +779,10 @@ class ProfileController : NewBaseController(R.layout.controller_profile) {
                     }
                     holder.binding.scope.alpha = HIGH_EMPHASIS_ALPHA
                 } else {
-                    holder.binding.userInfoEditText.isEnabled = false
-                    holder.binding.userInfoEditText.isFocusableInTouchMode = false
-                    holder.binding.userInfoEditText.isEnabled = false
-                    holder.binding.userInfoEditText.isCursorVisible = false
-                    holder.binding.userInfoEditText.backgroundTintList = ColorStateList.valueOf(Color.TRANSPARENT)
+                    holder.binding.userInfoEditTextEdit.isEnabled = false
+                    holder.binding.userInfoEditTextEdit.isFocusableInTouchMode = false
+                    holder.binding.userInfoEditTextEdit.isEnabled = false
+                    holder.binding.userInfoEditTextEdit.isCursorVisible = false
                     holder.binding.scope.setOnClickListener(null)
                     holder.binding.scope.alpha = MEDIUM_EMPHASIS_ALPHA
                 }
@@ -791,19 +795,19 @@ class ProfileController : NewBaseController(R.layout.controller_profile) {
             holder: ViewHolder,
             item: UserInfoDetailsItem
         ) {
-            holder.binding.userInfoEditText.setText(item.text)
-            holder.binding.userInfoEditText.hint = item.hint
-            holder.binding.userInfoEditText.addTextChangedListener(object : TextWatcher {
+            holder.binding.userInfoEditTextEdit.setText(item.text)
+            holder.binding.userInfoInputLayout.hint = item.hint
+            holder.binding.userInfoEditTextEdit.addTextChangedListener(object : TextWatcher {
                 override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
                     // unused atm
                 }
 
                 override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
                     if (controller.edit) {
-                        displayList!![holder.adapterPosition].text = holder.binding.userInfoEditText.text.toString()
+                        displayList!![holder.adapterPosition].text = holder.binding.userInfoEditTextEdit.text.toString()
                     } else {
                         filteredDisplayList[holder.adapterPosition].text =
-                            holder.binding.userInfoEditText.text.toString()
+                            holder.binding.userInfoEditTextEdit.text.toString()
                     }
                 }
 

+ 37 - 2
app/src/main/java/com/nextcloud/talk/controllers/SettingsController.kt

@@ -78,6 +78,7 @@ import com.nextcloud.talk.jobs.ContactAddressBookWorker.Companion.deleteAll
 import com.nextcloud.talk.models.json.generic.GenericOverall
 import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
 import com.nextcloud.talk.users.UserManager
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.LoggingUtils.sendMailWithAttachment
@@ -118,6 +119,9 @@ class SettingsController : NewBaseController(R.layout.controller_settings) {
     @Inject
     lateinit var currentUserProvider: CurrentUserProviderNew
 
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
     private var saveStateHandler: LovelySaveStateHandler? = null
     private var currentUser: User? = null
     private var credentials: String? = null
@@ -402,7 +406,8 @@ class SettingsController : NewBaseController(R.layout.controller_settings) {
                 .setIcon(
                     DisplayUtils.getTintedDrawable(
                         resources,
-                        R.drawable.ic_delete_black_24dp, R.color.bg_default
+                        R.drawable.ic_delete_black_24dp,
+                        R.color.bg_default
                     )
                 )
                 .setPositiveButtonColor(context!!.resources.getColor(R.color.nc_darkRed))
@@ -511,6 +516,34 @@ class SettingsController : NewBaseController(R.layout.controller_settings) {
 
                 )
         }
+
+        themeCategories()
+        themeSwitchPreferences()
+    }
+
+    private fun themeSwitchPreferences() {
+        binding.run {
+            listOf(
+                settingsScreenLock,
+                settingsScreenSecurity,
+                settingsIncognitoKeyboard,
+                settingsPhoneBookIntegration,
+                settingsReadPrivacy,
+                settingsProxyUseCredentials
+            ).forEach(viewThemeUtils::colorSwitchPreference)
+        }
+    }
+
+    private fun themeCategories() {
+        binding.run {
+            listOf(
+                settingsNotificationsCategory,
+                settingsAboutCategory,
+                settingsAdvancedCategory,
+                settingsAppearanceCategory,
+                settingsPrivacyCategory
+            ).forEach(viewThemeUtils::colorPreferenceCategory)
+        }
     }
 
     private fun setupProxyTypeSettings() {
@@ -952,7 +985,9 @@ class SettingsController : NewBaseController(R.layout.controller_settings) {
         val phoneNumber = textInputLayout.editText!!.text.toString()
         ncApi.setUserData(
             ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token),
-            ApiUtils.getUrlForUserData(currentUser!!.baseUrl, currentUser!!.userId), "phone", phoneNumber
+            ApiUtils.getUrlForUserData(currentUser!!.baseUrl, currentUser!!.userId),
+            "phone",
+            phoneNumber
         ).subscribeOn(Schedulers.io())
             .observeOn(AndroidSchedulers.mainThread())
             .subscribe(object : Observer<GenericOverall> {

+ 16 - 10
app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/EntryMenuController.kt

@@ -25,7 +25,7 @@ package com.nextcloud.talk.controllers.bottomsheet
 
 import android.content.ComponentName
 import android.content.Intent
-import android.graphics.PorterDuff
+import android.content.res.ColorStateList
 import android.os.Bundle
 import android.os.Parcelable
 import android.text.Editable
@@ -34,6 +34,7 @@ import android.text.TextUtils
 import android.text.TextWatcher
 import android.view.View
 import android.view.inputmethod.EditorInfo
+import androidx.core.content.res.ResourcesCompat
 import autodagger.AutoInjector
 import com.bluelinelabs.conductor.RouterTransaction
 import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
@@ -45,6 +46,8 @@ import com.nextcloud.talk.controllers.base.NewBaseController
 import com.nextcloud.talk.controllers.util.viewBinding
 import com.nextcloud.talk.databinding.ControllerEntryMenuBinding
 import com.nextcloud.talk.models.json.conversations.Conversation
+import com.nextcloud.talk.ui.theme.ServerTheme
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.users.UserManager
 import com.nextcloud.talk.utils.ShareUtils
 import com.nextcloud.talk.utils.UriUtils
@@ -71,6 +74,12 @@ class EntryMenuController(args: Bundle) :
     @Inject
     lateinit var userManager: UserManager
 
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
+    @Inject
+    lateinit var serverTheme: ServerTheme
+
     private val operation: ConversationOperationEnum
     private var conversation: Conversation? = null
     private var shareIntent: Intent? = null
@@ -125,17 +134,11 @@ class EntryMenuController(args: Bundle) :
                     rootView = view,
                     editText = binding.textEdit,
                     onEmojiPopupShownListener = {
-                        if (resources != null) {
-                            binding.smileyButton.setColorFilter(
-                                resources!!.getColor(R.color.colorPrimary),
-                                PorterDuff.Mode.SRC_IN
-                            )
-                        }
+                        viewThemeUtils.colorImageView(binding.smileyButton)
                     },
                     onEmojiPopupDismissListener = {
-                        binding.smileyButton.setColorFilter(
-                            resources!!.getColor(R.color.emoji_icons),
-                            PorterDuff.Mode.SRC_IN
+                        binding.smileyButton.imageTintList = ColorStateList.valueOf(
+                            ResourcesCompat.getColor(resources!!, R.color.medium_emphasis_text, context.theme)
                         )
                     },
                     onEmojiClickListener = {
@@ -171,6 +174,9 @@ class EntryMenuController(args: Bundle) :
             binding.textInputLayout.endIconMode = TextInputLayout.END_ICON_NONE
         }
 
+        viewThemeUtils.colorTextInputLayout(binding.textInputLayout)
+        viewThemeUtils.colorMaterialButtonText(binding.okButton)
+
         binding.textInputLayout.hint = labelText
         binding.textInputLayout.requestFocus()
 

+ 5 - 0
app/src/main/java/com/nextcloud/talk/data/user/UsersDao.kt

@@ -32,6 +32,7 @@ import androidx.room.Transaction
 import androidx.room.Update
 import com.nextcloud.talk.data.user.model.UserEntity
 import io.reactivex.Maybe
+import io.reactivex.Observable
 import io.reactivex.Single
 
 @Dao
@@ -41,6 +42,10 @@ abstract class UsersDao {
     @Query("SELECT * FROM User where current = 1")
     abstract fun getActiveUser(): Maybe<UserEntity>
 
+    // get active user
+    @Query("SELECT * FROM User where current = 1")
+    abstract fun getActiveUserObservable(): Observable<UserEntity>
+
     @Query("SELECT * FROM User where current = 1")
     abstract fun getActiveUserSynchronously(): UserEntity?
 

+ 2 - 0
app/src/main/java/com/nextcloud/talk/data/user/UsersRepository.kt

@@ -24,11 +24,13 @@ package com.nextcloud.talk.data.user
 
 import com.nextcloud.talk.data.user.model.User
 import io.reactivex.Maybe
+import io.reactivex.Observable
 import io.reactivex.Single
 
 @Suppress("TooManyFunctions")
 interface UsersRepository {
     fun getActiveUser(): Maybe<User>
+    fun getActiveUserObservable(): Observable<User>
     fun getUsers(): Single<List<User>>
     fun getUserWithId(id: Long): Maybe<User>
     fun getUserWithIdNotScheduledForDeletion(id: Long): Maybe<User>

+ 5 - 0
app/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.kt

@@ -24,6 +24,7 @@ package com.nextcloud.talk.data.user
 
 import com.nextcloud.talk.data.user.model.User
 import io.reactivex.Maybe
+import io.reactivex.Observable
 import io.reactivex.Single
 
 @Suppress("TooManyFunctions")
@@ -33,6 +34,10 @@ class UsersRepositoryImpl(private val usersDao: UsersDao) : UsersRepository {
         return usersDao.getActiveUser().map { UserMapper.toModel(it) }
     }
 
+    override fun getActiveUserObservable(): Observable<User> {
+        return usersDao.getActiveUserObservable().map { UserMapper.toModel(it) }
+    }
+
     override fun getUsers(): Single<List<User>> {
         return usersDao.getUsers().map { UserMapper.toModel(it) }
     }

+ 8 - 2
app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchActivity.kt

@@ -41,6 +41,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.controllers.ConversationsListController
 import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.databinding.ActivityMessageSearchBinding
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.bundle.BundleKeys
 import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
@@ -64,6 +65,9 @@ class MessageSearchActivity : BaseActivity() {
     @Inject
     lateinit var userProvider: CurrentUserProviderNew
 
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
     private lateinit var binding: ActivityMessageSearchBinding
     private lateinit var searchView: SearchView
 
@@ -105,7 +109,9 @@ class MessageSearchActivity : BaseActivity() {
         DisplayUtils.applyColorToStatusBar(
             this,
             ResourcesCompat.getColor(
-                resources, R.color.appbar, null
+                resources,
+                R.color.appbar,
+                null
             )
         )
         DisplayUtils.applyColorToNavigationBar(
@@ -154,7 +160,7 @@ class MessageSearchActivity : BaseActivity() {
             emptyList()
         }
         val newItems =
-            state.results.map { MessageResultItem(this, user, it) } + loadMoreItems
+            state.results.map { MessageResultItem(this, user, it, false, viewThemeUtils) } + loadMoreItems
 
         if (adapter != null) {
             adapter!!.updateDataSet(newItems)

+ 5 - 1
app/src/main/java/com/nextcloud/talk/models/json/capabilities/ThemingCapability.kt

@@ -43,6 +43,10 @@ data class ThemingCapability(
     var colorText: String?,
     @JsonField(name = ["color-element"])
     var colorElement: String?,
+    @JsonField(name = ["color-element-bright"])
+    var colorElementBright: String?,
+    @JsonField(name = ["color-element-dark"])
+    var colorElementDark: String?,
     @JsonField(name = ["logo"])
     var logo: String?,
     @JsonField(name = ["background"])
@@ -53,5 +57,5 @@ data class ThemingCapability(
     var backgroundDefault: Boolean?
 ) : Parcelable {
     // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
-    constructor() : this(null, null, null, null, null, null, null, null, null, null)
+    constructor() : this(null, null, null, null, null, null, null, null, null, null, null, null)
 }

+ 4 - 1
app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionViewHolder.kt

@@ -26,10 +26,12 @@ import android.text.TextWatcher
 import androidx.recyclerview.widget.RecyclerView
 import com.nextcloud.talk.R
 import com.nextcloud.talk.databinding.PollCreateOptionsItemBinding
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.EmojiTextInputEditText
 
 class PollCreateOptionViewHolder(
-    private val binding: PollCreateOptionsItemBinding
+    private val binding: PollCreateOptionsItemBinding,
+    private val viewThemeUtils: ViewThemeUtils
 ) : RecyclerView.ViewHolder(binding.root) {
 
     lateinit var optionText: EmojiTextInputEditText
@@ -48,6 +50,7 @@ class PollCreateOptionViewHolder(
         }
 
         binding.pollOptionTextEdit.setText(pollCreateOptionItem.pollOption)
+        viewThemeUtils.colorEditText(binding.pollOptionTextEdit)
 
         if (focus) {
             itemsListener.requestFocus(binding.pollOptionTextEdit)

+ 4 - 2
app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionsAdapter.kt

@@ -24,9 +24,11 @@ import android.view.LayoutInflater
 import android.view.ViewGroup
 import androidx.recyclerview.widget.RecyclerView
 import com.nextcloud.talk.databinding.PollCreateOptionsItemBinding
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 
 class PollCreateOptionsAdapter(
-    private val clickListener: PollCreateOptionsItemListener
+    private val clickListener: PollCreateOptionsItemListener,
+    private val viewThemeUtils: ViewThemeUtils
 ) : RecyclerView.Adapter<PollCreateOptionViewHolder>() {
 
     internal var list: ArrayList<PollCreateOptionItem> = ArrayList()
@@ -34,7 +36,7 @@ class PollCreateOptionsAdapter(
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PollCreateOptionViewHolder {
         val itemBinding = PollCreateOptionsItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
 
-        return PollCreateOptionViewHolder(itemBinding)
+        return PollCreateOptionViewHolder(itemBinding, viewThemeUtils)
     }
 
     override fun onBindViewHolder(holder: PollCreateOptionViewHolder, position: Int) {

+ 5 - 1
app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultHeaderViewHolder.kt

@@ -23,15 +23,19 @@ package com.nextcloud.talk.polls.adapters
 import android.annotation.SuppressLint
 import android.graphics.Typeface
 import com.nextcloud.talk.databinding.PollResultHeaderItemBinding
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 
 class PollResultHeaderViewHolder(
-    override val binding: PollResultHeaderItemBinding
+    override val binding: PollResultHeaderItemBinding,
+    private val viewThemeUtils: ViewThemeUtils
 ) : PollResultViewHolder(binding) {
 
     @SuppressLint("SetTextI18n")
     override fun bind(pollResultItem: PollResultItem, clickListener: PollResultItemClickListener) {
         val item = pollResultItem as PollResultHeaderItem
 
+        viewThemeUtils.colorProgressBar(binding.pollOptionBar)
+
         binding.root.setOnClickListener { clickListener.onClick() }
 
         binding.pollOptionText.text = item.name

+ 3 - 1
app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultsAdapter.kt

@@ -27,10 +27,12 @@ import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.databinding.PollResultHeaderItemBinding
 import com.nextcloud.talk.databinding.PollResultVoterItemBinding
 import com.nextcloud.talk.databinding.PollResultVotersOverviewItemBinding
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 
 class PollResultsAdapter(
     private val user: User,
     private val clickListener: PollResultItemClickListener,
+    private val viewThemeUtils: ViewThemeUtils
 ) : RecyclerView.Adapter<PollResultViewHolder>() {
     internal var list: MutableList<PollResultItem> = ArrayList()
 
@@ -43,7 +45,7 @@ class PollResultsAdapter(
                     LayoutInflater.from(parent.context), parent,
                     false
                 )
-                viewHolder = PollResultHeaderViewHolder(itemBinding)
+                viewHolder = PollResultHeaderViewHolder(itemBinding, viewThemeUtils)
             }
             PollResultVoterItem.VIEW_TYPE -> {
                 val itemBinding = PollResultVoterItemBinding.inflate(

+ 23 - 1
app/src/main/java/com/nextcloud/talk/polls/ui/PollCreateDialogFragment.kt

@@ -43,6 +43,7 @@ import com.nextcloud.talk.polls.adapters.PollCreateOptionItem
 import com.nextcloud.talk.polls.adapters.PollCreateOptionsAdapter
 import com.nextcloud.talk.polls.adapters.PollCreateOptionsItemListener
 import com.nextcloud.talk.polls.viewmodels.PollCreateViewModel
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import javax.inject.Inject
 
 @AutoInjector(NextcloudTalkApplication::class)
@@ -51,6 +52,9 @@ class PollCreateDialogFragment : DialogFragment(), PollCreateOptionsItemListener
     @Inject
     lateinit var viewModelFactory: ViewModelProvider.Factory
 
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
     private lateinit var binding: DialogPollCreateBinding
     private lateinit var viewModel: PollCreateViewModel
 
@@ -85,13 +89,31 @@ class PollCreateDialogFragment : DialogFragment(), PollCreateOptionsItemListener
 
         binding.pollCreateOptionsList.layoutManager = LinearLayoutManager(context)
 
-        adapter = PollCreateOptionsAdapter(this)
+        adapter = PollCreateOptionsAdapter(this, viewThemeUtils)
         binding.pollCreateOptionsList.adapter = adapter
 
+        themeDialog()
+
         setupListeners()
         setupStateObserver()
     }
 
+    private fun themeDialog() {
+        viewThemeUtils.colorTextViewText(binding.pollQuestion)
+        viewThemeUtils.colorTextViewText(binding.pollOptions)
+        viewThemeUtils.colorTextViewText(binding.pollSettings)
+
+        viewThemeUtils.colorEditText(binding.pollCreateQuestionTextEdit)
+
+        viewThemeUtils.colorMaterialButtonText(binding.pollAddOptionsItem)
+        // TODO button also needs a disabled state handling for colors
+        viewThemeUtils.colorMaterialButtonText(binding.pollDismiss)
+        viewThemeUtils.colorMaterialButtonBackground(binding.pollCreateButton)
+
+        viewThemeUtils.themeCheckbox(binding.pollPrivatePollCheckbox)
+        viewThemeUtils.themeCheckbox(binding.pollMultipleAnswersCheckbox)
+    }
+
     private fun setupListeners() {
         binding.pollAddOptionsItem.setOnClickListener {
             viewModel.addOption()

+ 13 - 2
app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt

@@ -38,6 +38,7 @@ import com.nextcloud.talk.polls.adapters.PollResultItemClickListener
 import com.nextcloud.talk.polls.adapters.PollResultsAdapter
 import com.nextcloud.talk.polls.viewmodels.PollMainViewModel
 import com.nextcloud.talk.polls.viewmodels.PollResultsViewModel
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import javax.inject.Inject
 
 @AutoInjector(NextcloudTalkApplication::class)
@@ -46,6 +47,9 @@ class PollResultsFragment : Fragment(), PollResultItemClickListener {
     @Inject
     lateinit var viewModelFactory: ViewModelProvider.Factory
 
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
     private lateinit var parentViewModel: PollMainViewModel
     lateinit var viewModel: PollResultsViewModel
 
@@ -82,17 +86,24 @@ class PollResultsFragment : Fragment(), PollResultItemClickListener {
         }
 
         viewModel.items.observe(viewLifecycleOwner) {
-            val adapter = PollResultsAdapter(parentViewModel.user, this).apply {
+            val adapter = PollResultsAdapter(parentViewModel.user, this, viewThemeUtils).apply {
                 if (it != null) {
                     list = it
                 }
             }
             binding.pollResultsList.adapter = adapter
         }
+
+        themeDialog()
+    }
+
+    private fun themeDialog() {
+        viewThemeUtils.colorMaterialButtonBackground(binding.editVoteButton)
+        viewThemeUtils.colorMaterialButtonText(binding.pollResultsEndPollButton)
     }
 
     private fun initAdapter() {
-        adapter = PollResultsAdapter(parentViewModel.user, this)
+        adapter = PollResultsAdapter(parentViewModel.user, this, viewThemeUtils)
         binding.pollResultsList.adapter = adapter
         binding.pollResultsList.layoutManager = LinearLayoutManager(context)
     }

+ 14 - 0
app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt

@@ -43,6 +43,7 @@ import com.nextcloud.talk.databinding.DialogPollVoteBinding
 import com.nextcloud.talk.polls.model.Poll
 import com.nextcloud.talk.polls.viewmodels.PollMainViewModel
 import com.nextcloud.talk.polls.viewmodels.PollVoteViewModel
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import javax.inject.Inject
 
 @AutoInjector(NextcloudTalkApplication::class)
@@ -51,6 +52,9 @@ class PollVoteFragment : Fragment() {
     @Inject
     lateinit var viewModelFactory: ViewModelProvider.Factory
 
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
     private lateinit var parentViewModel: PollMainViewModel
     lateinit var viewModel: PollVoteViewModel
 
@@ -117,6 +121,14 @@ class PollVoteFragment : Fragment() {
         binding.pollVoteEditDismiss.setOnClickListener {
             parentViewModel.dismissEditVotes()
         }
+
+        themeDialog()
+    }
+
+    private fun themeDialog() {
+        viewThemeUtils.colorMaterialButtonBackground(binding.pollVoteSubmitButton)
+        viewThemeUtils.colorMaterialButtonText(binding.pollVoteEndPollButton)
+        viewThemeUtils.colorMaterialButtonText(binding.pollVoteEditDismiss)
     }
 
     private fun updateDismissEditButton(showDismissEditButton: Boolean) {
@@ -136,6 +148,7 @@ class PollVoteFragment : Fragment() {
                 RadioButton(context).apply { text = option }
             }?.forEachIndexed { index, radioButton ->
                 radioButton.id = index
+                viewThemeUtils.themeRadioButton(radioButton)
                 makeOptionBoldIfSelfVoted(radioButton, poll, index)
                 binding.pollVoteRadioGroup.addView(radioButton)
 
@@ -156,6 +169,7 @@ class PollVoteFragment : Fragment() {
                     setLayoutParams(layoutParams)
                 }
             }?.forEachIndexed { index, checkBox ->
+                viewThemeUtils.themeCheckbox(checkBox)
                 checkBox.id = index
                 makeOptionBoldIfSelfVoted(checkBox, poll, index)
                 binding.voteOptionsCheckboxesWrapper.addView(checkBox)

+ 6 - 2
app/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.kt

@@ -44,6 +44,7 @@ import com.nextcloud.talk.remotefilebrowser.SelectionInterface
 import com.nextcloud.talk.remotefilebrowser.adapters.RemoteFileBrowserItemsAdapter
 import com.nextcloud.talk.remotefilebrowser.viewmodels.RemoteFileBrowserItemsViewModel
 import com.nextcloud.talk.ui.dialog.SortingOrderDialogFragment
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.FileSortOrder
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_MIME_TYPE_FILTER
@@ -59,6 +60,9 @@ class RemoteFileBrowserActivity : AppCompatActivity(), SelectionInterface, Swipe
     @Inject
     lateinit var currentUserProvider: CurrentUserProviderNew
 
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
     private lateinit var binding: ActivityRemoteFileBrowserBinding
     private lateinit var viewModel: RemoteFileBrowserItemsViewModel
 
@@ -91,8 +95,7 @@ class RemoteFileBrowserActivity : AppCompatActivity(), SelectionInterface, Swipe
         initViewModel(mimeTypeSelectionFilter)
 
         binding.swipeRefreshList.setOnRefreshListener(this)
-        binding.swipeRefreshList.setColorSchemeResources(R.color.colorPrimary)
-        binding.swipeRefreshList.setProgressBackgroundColorSchemeResource(R.color.refresh_spinner_background)
+        viewThemeUtils.themeSwipeRefreshLayout(binding.swipeRefreshList)
 
         binding.pathNavigationBackButton.setOnClickListener { viewModel.navigateUp() }
         binding.sortButton.setOnClickListener { changeSorting() }
@@ -160,6 +163,7 @@ class RemoteFileBrowserActivity : AppCompatActivity(), SelectionInterface, Swipe
             mimeTypeSelectionFilter = mimeTypeSelectionFilter,
             user = currentUserProvider.currentUser.blockingGet(),
             selectionInterface = this,
+            viewThemeUtils = viewThemeUtils,
             onItemClicked = viewModel::onItemClicked
         )
         adapter.items = remoteFileBrowserItems

+ 6 - 3
app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsAdapter.kt

@@ -28,19 +28,20 @@ import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.databinding.RvItemBrowserFileBinding
 import com.nextcloud.talk.remotefilebrowser.SelectionInterface
 import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 
 class RemoteFileBrowserItemsAdapter(
     private val showGrid: Boolean = false,
     private val mimeTypeSelectionFilter: String? = null,
     private val user: User,
     private val selectionInterface: SelectionInterface,
+    private val viewThemeUtils: ViewThemeUtils,
     private val onItemClicked: (RemoteFileBrowserItem) -> Unit
 ) : RecyclerView.Adapter<RemoteFileBrowserItemsViewHolder>() {
 
     var items: List<RemoteFileBrowserItem> = emptyList()
 
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RemoteFileBrowserItemsViewHolder {
-
         return if (showGrid) {
             RemoteFileBrowserItemsListViewHolder(
                 RvItemBrowserFileBinding.inflate(
@@ -50,7 +51,8 @@ class RemoteFileBrowserItemsAdapter(
                 ),
                 mimeTypeSelectionFilter,
                 user,
-                selectionInterface
+                selectionInterface,
+                viewThemeUtils
             ) {
                 onItemClicked(items[it])
             }
@@ -63,7 +65,8 @@ class RemoteFileBrowserItemsAdapter(
                 ),
                 mimeTypeSelectionFilter,
                 user,
-                selectionInterface
+                selectionInterface,
+                viewThemeUtils
             ) {
                 onItemClicked(items[it])
             }

+ 4 - 6
app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsListViewHolder.kt

@@ -22,7 +22,6 @@ package com.nextcloud.talk.remotefilebrowser.adapters
 
 import android.text.format.Formatter
 import android.view.View
-import androidx.appcompat.content.res.AppCompatResources
 import autodagger.AutoInjector
 import com.facebook.drawee.backends.pipeline.Fresco
 import com.facebook.drawee.interfaces.DraweeController
@@ -33,10 +32,10 @@ import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.databinding.RvItemBrowserFileBinding
 import com.nextcloud.talk.remotefilebrowser.SelectionInterface
 import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.DateUtils.getLocalDateTimeStringFromTimestamp
 import com.nextcloud.talk.utils.DisplayUtils
-import com.nextcloud.talk.utils.DrawableUtils.getDrawableResourceIdForMimeType
 import com.nextcloud.talk.utils.Mimetype.FOLDER
 
 @AutoInjector(NextcloudTalkApplication::class)
@@ -45,6 +44,7 @@ class RemoteFileBrowserItemsListViewHolder(
     mimeTypeSelectionFilter: String?,
     currentUser: User,
     selectionInterface: SelectionInterface,
+    private val viewThemeUtils: ViewThemeUtils,
     onItemClicked: (Int) -> Unit
 ) : RemoteFileBrowserItemsViewHolder(binding, mimeTypeSelectionFilter, currentUser, selectionInterface) {
 
@@ -66,7 +66,6 @@ class RemoteFileBrowserItemsListViewHolder(
     }
 
     override fun onBind(item: RemoteFileBrowserItem) {
-
         super.onBind(item)
 
         binding.fileIcon.controller = null
@@ -99,9 +98,7 @@ class RemoteFileBrowserItemsListViewHolder(
         binding.fileIcon
             .hierarchy
             .setPlaceholderImage(
-                AppCompatResources.getDrawable(
-                    binding.fileIcon.context, getDrawableResourceIdForMimeType(item.mimeType)
-                )
+                viewThemeUtils.getPlaceholderImage(binding.root.context, item.mimeType)
             )
 
         if (item.hasPreview) {
@@ -132,6 +129,7 @@ class RemoteFileBrowserItemsListViewHolder(
     private fun setSelectability() {
         if (selectable) {
             binding.selectFileCheckbox.visibility = View.VISIBLE
+            viewThemeUtils.themeCheckbox(binding.selectFileCheckbox)
         } else {
             binding.selectFileCheckbox.visibility = View.GONE
         }

+ 11 - 3
app/src/main/java/com/nextcloud/talk/shareditems/activities/SharedItemsActivity.kt

@@ -41,6 +41,7 @@ import com.nextcloud.talk.databinding.ActivitySharedItemsBinding
 import com.nextcloud.talk.shareditems.adapters.SharedItemsAdapter
 import com.nextcloud.talk.shareditems.model.SharedItemType
 import com.nextcloud.talk.shareditems.viewmodels.SharedItemsViewModel
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
@@ -53,6 +54,9 @@ class SharedItemsActivity : AppCompatActivity() {
     @Inject
     lateinit var viewModelFactory: ViewModelProvider.Factory
 
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
     private lateinit var binding: ActivitySharedItemsBinding
     private lateinit var viewModel: SharedItemsViewModel
 
@@ -72,7 +76,9 @@ class SharedItemsActivity : AppCompatActivity() {
         DisplayUtils.applyColorToStatusBar(
             this,
             ResourcesCompat.getColor(
-                resources, R.color.appbar, null
+                resources,
+                R.color.appbar,
+                null
             )
         )
         DisplayUtils.applyColorToNavigationBar(
@@ -130,7 +136,8 @@ class SharedItemsActivity : AppCompatActivity() {
                     showGrid,
                     user,
                     roomToken,
-                    isUserConversationOwnerOrModerator
+                    isUserConversationOwnerOrModerator,
+                    viewThemeUtils
                 ).apply {
                     items = sharedMediaItems.items
                 }
@@ -142,6 +149,8 @@ class SharedItemsActivity : AppCompatActivity() {
             }
             else -> {}
         }
+
+        viewThemeUtils.colorTabLayout(binding.sharedItemsTabs)
     }
 
     private fun clearEmptyLoading() {
@@ -161,7 +170,6 @@ class SharedItemsActivity : AppCompatActivity() {
     }
 
     private fun initTabs(sharedItemTypes: Set<SharedItemType>) {
-
         binding.sharedItemsTabs.removeAllTabs()
 
         if (sharedItemTypes.contains(SharedItemType.MEDIA)) {

+ 7 - 4
app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsAdapter.kt

@@ -37,18 +37,19 @@ import com.nextcloud.talk.shareditems.model.SharedItem
 import com.nextcloud.talk.shareditems.model.SharedLocationItem
 import com.nextcloud.talk.shareditems.model.SharedOtherItem
 import com.nextcloud.talk.shareditems.model.SharedPollItem
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 
 class SharedItemsAdapter(
     private val showGrid: Boolean,
     private val user: User,
     private val roomToken: String,
-    private val isUserConversationOwnerOrModerator: Boolean
+    private val isUserConversationOwnerOrModerator: Boolean,
+    private val viewThemeUtils: ViewThemeUtils
 ) : RecyclerView.Adapter<SharedItemsViewHolder>() {
 
     var items: List<SharedItem> = emptyList()
 
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SharedItemsViewHolder {
-
         return if (showGrid) {
             SharedItemsGridViewHolder(
                 SharedItemGridBinding.inflate(
@@ -56,7 +57,8 @@ class SharedItemsAdapter(
                     parent,
                     false
                 ),
-                user
+                user,
+                viewThemeUtils
             )
         } else {
             SharedItemsListViewHolder(
@@ -65,7 +67,8 @@ class SharedItemsAdapter(
                     parent,
                     false
                 ),
-                user
+                user,
+                viewThemeUtils
             )
         }
     }

+ 4 - 2
app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsGridViewHolder.kt

@@ -27,11 +27,13 @@ import android.widget.ProgressBar
 import com.facebook.drawee.view.SimpleDraweeView
 import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.databinding.SharedItemGridBinding
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 
 class SharedItemsGridViewHolder(
     override val binding: SharedItemGridBinding,
-    user: User
-) : SharedItemsViewHolder(binding, user) {
+    user: User,
+    viewThemeUtils: ViewThemeUtils
+) : SharedItemsViewHolder(binding, user, viewThemeUtils) {
 
     override val image: SimpleDraweeView
         get() = binding.image

+ 4 - 3
app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsListViewHolder.kt

@@ -38,11 +38,13 @@ import com.nextcloud.talk.shareditems.model.SharedItem
 import com.nextcloud.talk.shareditems.model.SharedLocationItem
 import com.nextcloud.talk.shareditems.model.SharedOtherItem
 import com.nextcloud.talk.shareditems.model.SharedPollItem
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 
 class SharedItemsListViewHolder(
     override val binding: SharedItemListBinding,
-    user: User
-) : SharedItemsViewHolder(binding, user) {
+    user: User,
+    viewThemeUtils: ViewThemeUtils
+) : SharedItemsViewHolder(binding, user, viewThemeUtils) {
 
     override val image: SimpleDraweeView
         get() = binding.fileImage
@@ -52,7 +54,6 @@ class SharedItemsListViewHolder(
         get() = binding.progressBar
 
     override fun onBind(item: SharedFileItem) {
-
         super.onBind(item)
 
         binding.fileName.text = item.name

+ 4 - 14
app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsViewHolder.kt

@@ -23,12 +23,10 @@
 package com.nextcloud.talk.shareditems.adapters
 
 import android.content.Context
-import android.graphics.drawable.Drawable
 import android.net.Uri
 import android.util.Log
 import android.view.View
 import android.widget.ProgressBar
-import androidx.core.content.ContextCompat
 import androidx.recyclerview.widget.RecyclerView
 import androidx.viewbinding.ViewBinding
 import com.facebook.drawee.backends.pipeline.Fresco
@@ -43,16 +41,17 @@ import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.shareditems.model.SharedDeckCardItem
 import com.nextcloud.talk.shareditems.model.SharedFileItem
 import com.nextcloud.talk.shareditems.model.SharedItem
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.shareditems.model.SharedLocationItem
 import com.nextcloud.talk.shareditems.model.SharedOtherItem
 import com.nextcloud.talk.shareditems.model.SharedPollItem
 import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.DrawableUtils
 import com.nextcloud.talk.utils.FileViewerUtils
 
 abstract class SharedItemsViewHolder(
     open val binding: ViewBinding,
-    internal val user: User
+    internal val user: User,
+    private val viewThemeUtils: ViewThemeUtils
 ) : RecyclerView.ViewHolder(binding.root) {
 
     companion object {
@@ -71,7 +70,7 @@ abstract class SharedItemsViewHolder(
     )
 
     open fun onBind(item: SharedFileItem) {
-        image.hierarchy.setPlaceholderImage(staticImage(item.mimeType, image))
+        image.hierarchy.setPlaceholderImage(viewThemeUtils.getPlaceholderImage(image.context, item.mimeType))
         if (item.previewAvailable) {
             image.controller = configurePreview(item)
         }
@@ -107,7 +106,6 @@ abstract class SharedItemsViewHolder(
     }
 
     private fun configurePreview(item: SharedFileItem): DraweeController {
-
         val imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(item.previewLink))
             .setProgressiveRenderingEnabled(true)
             .setRotationOptions(RotationOptions.autoRotate())
@@ -136,12 +134,4 @@ abstract class SharedItemsViewHolder(
     open fun onBind(item: SharedOtherItem) {}
 
     open fun onBind(item: SharedDeckCardItem) {}
-
-    private fun staticImage(
-        mimeType: String?,
-        image: SimpleDraweeView
-    ): Drawable {
-        val drawableResourceId = DrawableUtils.getDrawableResourceIdForMimeType(mimeType)
-        return ContextCompat.getDrawable(image.context, drawableResourceId)!!
-    }
 }

+ 24 - 41
app/src/main/java/com/nextcloud/talk/ui/dialog/AudioOutputDialog.kt

@@ -24,18 +24,33 @@ import android.os.Bundle
 import android.util.Log
 import android.view.View
 import android.view.ViewGroup
-import androidx.core.content.ContextCompat
+import autodagger.AutoInjector
 import com.google.android.material.bottomsheet.BottomSheetBehavior
 import com.google.android.material.bottomsheet.BottomSheetDialog
 import com.nextcloud.talk.R
 import com.nextcloud.talk.activities.CallActivity
+import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.databinding.DialogAudioOutputBinding
+import com.nextcloud.talk.ui.theme.ServerTheme
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.webrtc.WebRtcAudioManager
+import javax.inject.Inject
 
+@AutoInjector(NextcloudTalkApplication::class)
 class AudioOutputDialog(val callActivity: CallActivity) : BottomSheetDialog(callActivity) {
 
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
+    @Inject
+    lateinit var serverTheme: ServerTheme
+
     private lateinit var dialogAudioOutputBinding: DialogAudioOutputBinding
 
+    init {
+        NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this)
+    }
+
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         dialogAudioOutputBinding = DialogAudioOutputBinding.inflate(layoutInflater)
@@ -82,55 +97,23 @@ class AudioOutputDialog(val callActivity: CallActivity) : BottomSheetDialog(call
     private fun highlightActiveOutputChannel() {
         when (callActivity.audioManager?.currentAudioDevice) {
             WebRtcAudioManager.AudioDevice.BLUETOOTH -> {
-                dialogAudioOutputBinding.audioOutputBluetoothIcon.setColorFilter(
-                    ContextCompat.getColor(
-                        context,
-                        R.color.colorPrimary
-                    ),
-                    android.graphics.PorterDuff.Mode.SRC_IN
-                )
-                dialogAudioOutputBinding.audioOutputBluetoothText.setTextColor(
-                    callActivity.resources.getColor(R.color.colorPrimary)
-                )
+                viewThemeUtils.colorImageView(dialogAudioOutputBinding.audioOutputBluetoothIcon)
+                dialogAudioOutputBinding.audioOutputBluetoothText.setTextColor(serverTheme.primaryColor)
             }
 
             WebRtcAudioManager.AudioDevice.SPEAKER_PHONE -> {
-                dialogAudioOutputBinding.audioOutputSpeakerIcon.setColorFilter(
-                    ContextCompat.getColor(
-                        context,
-                        R.color.colorPrimary
-                    ),
-                    android.graphics.PorterDuff.Mode.SRC_IN
-                )
-                dialogAudioOutputBinding.audioOutputSpeakerText.setTextColor(
-                    callActivity.resources.getColor(R.color.colorPrimary)
-                )
+                viewThemeUtils.colorImageView(dialogAudioOutputBinding.audioOutputSpeakerIcon)
+                dialogAudioOutputBinding.audioOutputSpeakerText.setTextColor(serverTheme.primaryColor)
             }
 
             WebRtcAudioManager.AudioDevice.EARPIECE -> {
-                dialogAudioOutputBinding.audioOutputEarspeakerIcon.setColorFilter(
-                    ContextCompat.getColor(
-                        context,
-                        R.color.colorPrimary
-                    ),
-                    android.graphics.PorterDuff.Mode.SRC_IN
-                )
-                dialogAudioOutputBinding.audioOutputEarspeakerText.setTextColor(
-                    callActivity.resources.getColor(R.color.colorPrimary)
-                )
+                viewThemeUtils.colorImageView(dialogAudioOutputBinding.audioOutputEarspeakerIcon)
+                dialogAudioOutputBinding.audioOutputEarspeakerText.setTextColor(serverTheme.primaryColor)
             }
 
             WebRtcAudioManager.AudioDevice.WIRED_HEADSET -> {
-                dialogAudioOutputBinding.audioOutputWiredHeadsetIcon.setColorFilter(
-                    ContextCompat.getColor(
-                        context,
-                        R.color.colorPrimary
-                    ),
-                    android.graphics.PorterDuff.Mode.SRC_IN
-                )
-                dialogAudioOutputBinding.audioOutputWiredHeadsetText.setTextColor(
-                    callActivity.resources.getColor(R.color.colorPrimary)
-                )
+                viewThemeUtils.colorImageView(dialogAudioOutputBinding.audioOutputWiredHeadsetIcon)
+                dialogAudioOutputBinding.audioOutputWiredHeadsetText.setTextColor(serverTheme.primaryColor)
             }
 
             else -> Log.d(TAG, "AudioOutputDialog doesn't know this AudioDevice")

+ 7 - 0
app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java

@@ -51,6 +51,7 @@ import com.nextcloud.talk.models.json.status.Status;
 import com.nextcloud.talk.models.json.status.StatusOverall;
 import com.nextcloud.talk.ui.StatusDrawable;
 import com.nextcloud.talk.users.UserManager;
+import com.nextcloud.talk.ui.theme.ViewThemeUtils;
 import com.nextcloud.talk.utils.ApiUtils;
 import com.nextcloud.talk.utils.DisplayUtils;
 import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew;
@@ -87,6 +88,9 @@ public class ChooseAccountDialogFragment extends DialogFragment {
     @Inject
     NcApi ncApi;
 
+    @Inject
+    ViewThemeUtils viewThemeUtils;
+
     private DialogChooseAccountBinding binding;
     private View dialogView;
 
@@ -120,6 +124,9 @@ public class ChooseAccountDialogFragment extends DialogFragment {
             binding.currentAccount.ticker.setVisibility(View.GONE);
             binding.currentAccount.account.setText((Uri.parse(user.getBaseUrl()).getHost()));
 
+            viewThemeUtils.colorImageView(binding.currentAccount.accountMenu);
+
+
             if (user.getBaseUrl() != null &&
                 (user.getBaseUrl().startsWith("http://") || user.getBaseUrl().startsWith("https://"))) {
                 binding.currentAccount.userIcon.setVisibility(View.VISIBLE);

+ 28 - 27
app/src/main/java/com/nextcloud/talk/ui/dialog/SetStatusDialogFragment.kt

@@ -34,12 +34,15 @@ import android.view.inputmethod.InputMethodManager
 import android.widget.AdapterView
 import android.widget.AdapterView.OnItemSelectedListener
 import android.widget.ArrayAdapter
+import android.widget.ImageView
+import android.widget.TextView
 import androidx.appcompat.app.AlertDialog
 import androidx.core.widget.doAfterTextChanged
 import androidx.fragment.app.DialogFragment
 import androidx.recyclerview.widget.LinearLayoutManager
 import autodagger.AutoInjector
 import com.bluelinelabs.logansquare.LoganSquare
+import com.google.android.material.card.MaterialCardView
 import com.nextcloud.talk.R
 import com.nextcloud.talk.adapters.PredefinedStatusClickListener
 import com.nextcloud.talk.adapters.PredefinedStatusListAdapter
@@ -53,6 +56,7 @@ import com.nextcloud.talk.models.json.status.Status
 import com.nextcloud.talk.models.json.status.StatusType
 import com.nextcloud.talk.models.json.status.predefined.PredefinedStatus
 import com.nextcloud.talk.models.json.status.predefined.PredefinedStatusOverall
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.DisplayUtils
 import com.vanniktech.emoji.EmojiPopup
@@ -105,6 +109,9 @@ class SetStatusDialogFragment :
     @Inject
     lateinit var ncApi: NcApi
 
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
     lateinit var credentials: String
 
     override fun onCreate(savedInstanceState: Bundle?) {
@@ -234,8 +241,8 @@ class SetStatusDialogFragment :
             }
         }
 
-        binding.clearStatus.setTextColor(resources.getColor(R.color.colorPrimary))
-        binding.setStatus.setBackgroundColor(resources.getColor(R.color.colorPrimary))
+        viewThemeUtils.colorMaterialButtonText(binding.clearStatus)
+        viewThemeUtils.colorMaterialButtonBackground(binding.setStatus)
 
         binding.customStatusInput.highlightColor = resources.getColor(R.color.colorPrimary)
 
@@ -258,7 +265,6 @@ class SetStatusDialogFragment :
 
     @Suppress("ComplexMethod")
     private fun setClearStatusAfterValue(item: Int) {
-
         val currentTime = System.currentTimeMillis() / ONE_SECOND_IN_MILLIS
 
         when (item) {
@@ -310,7 +316,6 @@ class SetStatusDialogFragment :
     }
 
     private fun clearAtToUnixTime(clearAt: ClearAt?): Long {
-
         var returnValue = -1L
 
         if (clearAt != null) {
@@ -400,25 +405,18 @@ class SetStatusDialogFragment :
 
     private fun visualizeStatus(statusType: StatusType) {
         clearTopStatus()
-        when (statusType) {
-            StatusType.ONLINE -> {
-                binding.onlineStatus.setCardBackgroundColor(resources.getColor(R.color.colorPrimary))
-                binding.onlineHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text_dark_background))
-            }
-            StatusType.AWAY -> {
-                binding.awayStatus.setCardBackgroundColor(resources.getColor(R.color.colorPrimary))
-                binding.awayHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text_dark_background))
-            }
-            StatusType.DND -> {
-                binding.dndStatus.setCardBackgroundColor(resources.getColor(R.color.colorPrimary))
-                binding.dndHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text_dark_background))
-            }
-            StatusType.INVISIBLE -> {
-                binding.invisibleStatus.setCardBackgroundColor(resources.getColor(R.color.colorPrimary))
-                binding.invisibleHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text_dark_background))
+        val views: Triple<MaterialCardView, TextView, ImageView> = when (statusType) {
+            StatusType.ONLINE -> Triple(binding.onlineStatus, binding.onlineHeadline, binding.onlineIcon)
+            StatusType.AWAY -> Triple(binding.awayStatus, binding.awayHeadline, binding.awayIcon)
+            StatusType.DND -> Triple(binding.dndStatus, binding.dndHeadline, binding.dndIcon)
+            StatusType.INVISIBLE -> Triple(binding.invisibleStatus, binding.invisibleHeadline, binding.invisibleIcon)
+            else -> {
+                Log.d(TAG, "unknown status")
+                return
             }
-            else -> Log.d(TAG, "unknown status")
         }
+        viewThemeUtils.colorCardViewBackground(views.first)
+        viewThemeUtils.colorTextViewText(views.second)
     }
 
     private fun clearTopStatus() {
@@ -433,11 +431,15 @@ class SetStatusDialogFragment :
             binding.awayHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text))
             binding.dndHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text))
             binding.invisibleHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text))
+
+            binding.onlineIcon.imageTintList = null
+            binding.awayIcon.imageTintList = null
+            binding.dndIcon.imageTintList = null
+            binding.invisibleIcon.imageTintList = null
         }
     }
 
     private fun setStatusMessage() {
-
         val inputText = binding.customStatusInput.text.toString().ifEmpty { "" }
         // The endpoint '/message/custom' expects a valid emoji as string or null
         val statusIcon = binding.emoji.text.toString().ifEmpty { null }
@@ -446,7 +448,6 @@ class SetStatusDialogFragment :
             selectedPredefinedStatus!!.message != inputText ||
             selectedPredefinedStatus!!.icon != binding.emoji.text.toString()
         ) {
-
             ncApi.setCustomStatusMessage(
                 credentials,
                 ApiUtils.getUrlForSetCustomStatus(currentUser?.baseUrl),
@@ -476,12 +477,13 @@ class SetStatusDialogFragment :
                     }
                 })
         } else {
-
             val clearAt = clearAtToUnixTime(selectedPredefinedStatus!!.clearAt)
 
             ncApi.setPredefinedStatusMessage(
-                credentials, ApiUtils.getUrlForSetPredefinedStatus(currentUser?.baseUrl),
-                selectedPredefinedStatus!!.id, if (clearAt == -1L) null else clearAt
+                credentials,
+                ApiUtils.getUrlForSetPredefinedStatus(currentUser?.baseUrl),
+                selectedPredefinedStatus!!.id,
+                if (clearAt == -1L) null else clearAt
             )
                 .subscribeOn(Schedulers.io())
                 .observeOn(AndroidSchedulers.mainThread())?.subscribe(object : Observer<GenericOverall> {
@@ -506,7 +508,6 @@ class SetStatusDialogFragment :
     }
 
     override fun onClick(predefinedStatus: PredefinedStatus) {
-
         selectedPredefinedStatus = predefinedStatus
 
         clearAt = clearAtToUnixTime(predefinedStatus.clearAt)

+ 4 - 1
app/src/main/java/com/nextcloud/talk/ui/dialog/ShowReactionsDialog.kt

@@ -51,6 +51,7 @@ import com.nextcloud.talk.models.json.chat.ChatMessage
 import com.nextcloud.talk.models.json.conversations.Conversation
 import com.nextcloud.talk.models.json.generic.GenericOverall
 import com.nextcloud.talk.models.json.reactions.ReactionsOverall
+import com.nextcloud.talk.ui.theme.ServerTheme
 import com.nextcloud.talk.utils.ApiUtils
 import io.reactivex.Observer
 import io.reactivex.android.schedulers.AndroidSchedulers
@@ -65,7 +66,8 @@ class ShowReactionsDialog(
     private val chatMessage: ChatMessage,
     private val user: User?,
     private val hasChatPermission: Boolean,
-    private val ncApi: NcApi
+    private val ncApi: NcApi,
+    private val serverTheme: ServerTheme
 ) : BottomSheetDialog(activity), ReactionItemClickListener {
 
     private lateinit var binding: DialogMessageReactionsBinding
@@ -96,6 +98,7 @@ class ShowReactionsDialog(
         adapter?.list?.clear()
         if (chatMessage.reactions != null && chatMessage.reactions!!.isNotEmpty()) {
             var reactionsTotal = 0
+            binding.emojiReactionsTabs.setSelectedTabIndicatorColor(serverTheme.primaryColor)
             for ((emoji, amount) in chatMessage.reactions!!) {
                 reactionsTotal = reactionsTotal.plus(amount as Int)
                 val tab: TabLayout.Tab = binding.emojiReactionsTabs.newTab() // Create a new Tab names "First Tab"

+ 8 - 8
app/src/main/java/com/nextcloud/talk/ui/dialog/SortingOrderDialogFragment.java

@@ -33,9 +33,9 @@ import android.widget.TextView;
 
 import com.google.android.material.button.MaterialButton;
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.nextcloud.talk.R;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
 import com.nextcloud.talk.databinding.SortingOrderFragmentBinding;
+import com.nextcloud.talk.ui.theme.ViewThemeUtils;
 import com.nextcloud.talk.utils.FileSortOrder;
 import com.nextcloud.talk.utils.preferences.AppPreferences;
 
@@ -46,7 +46,6 @@ import javax.inject.Inject;
 import androidx.annotation.NonNull;
 import androidx.fragment.app.DialogFragment;
 import autodagger.AutoInjector;
-import kotlin.jvm.JvmField;
 
 /**
  * Dialog to show and choose the sorting order for the file listing.
@@ -60,9 +59,11 @@ public class SortingOrderDialogFragment extends DialogFragment implements View.O
     private static final String KEY_SORT_ORDER = "SORT_ORDER";
 
     @Inject
-    @JvmField
     AppPreferences appPreferences;
 
+    @Inject
+    ViewThemeUtils viewThemeUtils;
+
     private SortingOrderFragmentBinding binding;
     private View dialogView;
 
@@ -119,7 +120,7 @@ public class SortingOrderDialogFragment extends DialogFragment implements View.O
      * find all relevant UI elements and set their values.
      */
     private void setupDialogElements() {
-        binding.cancel.setTextColor(getResources().getColor(R.color.colorPrimary));
+        viewThemeUtils.colorMaterialButtonText(binding.cancel);
 
         taggedViews = new View[12];
         taggedViews[0] = binding.sortByNameAscending;
@@ -154,18 +155,17 @@ public class SortingOrderDialogFragment extends DialogFragment implements View.O
      * tints the icon reflecting the actual sorting choice in the apps primary color.
      */
     private void setupActiveOrderSelection() {
-        final int color = getResources().getColor(R.color.colorPrimary);
-        Log.i("SortOrder", "currentSortOrderName="+currentSortOrderName);
+        Log.i("SortOrder", "currentSortOrderName=" + currentSortOrderName);
         for (View view : taggedViews) {
             Log.i("SortOrder", ((FileSortOrder) view.getTag()).getName());
             if (!((FileSortOrder) view.getTag()).getName().equals(currentSortOrderName)) {
                 continue;
             }
             if (view instanceof MaterialButton) {
-                ((MaterialButton) view).setIconTintResource(R.color.colorPrimary);
+                viewThemeUtils.colorMaterialButtonText((MaterialButton) view);
             }
             if (view instanceof TextView) {
-                ((TextView) view).setTextColor(color);
+                viewThemeUtils.colorTextViewElement((TextView) view);
                 ((TextView) view).setTypeface(Typeface.DEFAULT_BOLD);
             }
         }

+ 53 - 0
app/src/main/java/com/nextcloud/talk/ui/theme/ServerTheme.kt

@@ -0,0 +1,53 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.ui.theme
+
+import androidx.annotation.ColorInt
+
+interface ServerTheme {
+    @get:ColorInt
+    val primaryColor: Int
+
+    /**
+     * Default element color
+     */
+    @get:ColorInt
+    val colorElement: Int
+
+    /**
+     * Element color for bright backgrounds
+     */
+    @get:ColorInt
+    val colorElementBright: Int
+
+    /**
+     * Element color for dark backgrounds
+     */
+    @get:ColorInt
+    val colorElementDark: Int
+
+    /**
+     * Text color for elements
+     */
+    @get:ColorInt
+    val colorText: Int
+}

+ 48 - 0
app/src/main/java/com/nextcloud/talk/ui/theme/ServerThemeImpl.kt

@@ -0,0 +1,48 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * @author Andy Scherzinger
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.ui.theme
+
+import com.nextcloud.talk.R
+import com.nextcloud.talk.models.json.capabilities.ThemingCapability
+import com.nextcloud.talk.utils.ui.ColorUtil
+
+internal class ServerThemeImpl(themingCapability: ThemingCapability?, colorUtil: ColorUtil) :
+    ServerTheme {
+
+    override val primaryColor: Int
+    override val colorElement: Int
+    override val colorElementBright: Int
+    override val colorElementDark: Int
+    override val colorText: Int
+
+    init {
+        primaryColor = colorUtil.getNullSafeColorWithFallbackRes(themingCapability?.color, R.color.colorPrimary)
+
+        colorElement = colorUtil.getNullSafeColor(themingCapability?.colorElement, primaryColor)
+        colorElementBright = colorUtil.getNullSafeColor(themingCapability?.colorElementBright, primaryColor)
+        colorElementDark = colorUtil.getNullSafeColor(themingCapability?.colorElementDark, primaryColor)
+
+        colorText = colorUtil.getTextColor(themingCapability?.colorText, primaryColor)
+    }
+}

+ 31 - 0
app/src/main/java/com/nextcloud/talk/ui/theme/ServerThemeProvider.kt

@@ -0,0 +1,31 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.ui.theme
+
+import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.models.json.capabilities.Capabilities
+
+interface ServerThemeProvider {
+    fun getServerThemeForUser(user: User?): ServerTheme
+    fun getServerThemeForCapabilities(capabilities: Capabilities?): ServerTheme
+    fun getServerThemeForCurrentUser(): ServerTheme
+}

+ 65 - 0
app/src/main/java/com/nextcloud/talk/ui/theme/ServerThemeProviderImpl.kt

@@ -0,0 +1,65 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * @author Andy Scherzinger
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.ui.theme
+
+import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.models.json.capabilities.Capabilities
+import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
+import com.nextcloud.talk.utils.ui.ColorUtil
+import java.util.concurrent.ConcurrentHashMap
+import javax.inject.Inject
+
+internal class ServerThemeProviderImpl @Inject constructor(
+    private val userProvider: CurrentUserProviderNew,
+    private val colorUtil: ColorUtil
+) : ServerThemeProvider {
+
+    private val themeCache: ConcurrentHashMap<String, ServerTheme> = ConcurrentHashMap()
+
+    override fun getServerThemeForUser(user: User?): ServerTheme {
+        val url: String = if (user?.baseUrl != null) {
+            user.baseUrl!!
+        } else {
+            FALLBACK_URL
+        }
+
+        if (!themeCache.containsKey(url)) {
+            themeCache[url] = getServerThemeForCapabilities(user?.capabilities)
+        }
+
+        return themeCache[url]!!
+    }
+
+    override fun getServerThemeForCurrentUser(): ServerTheme {
+        return getServerThemeForUser(userProvider.currentUser.blockingGet())
+    }
+
+    override fun getServerThemeForCapabilities(capabilities: Capabilities?): ServerTheme {
+        return ServerThemeImpl(capabilities?.themingCapability, colorUtil)
+    }
+
+    companion object {
+        const val FALLBACK_URL = "NULL"
+    }
+}

+ 44 - 0
app/src/main/java/com/nextcloud/talk/ui/theme/ThemeModule.kt

@@ -0,0 +1,44 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.ui.theme
+
+import com.nextcloud.talk.dagger.modules.ContextModule
+import com.nextcloud.talk.utils.database.user.UserModule
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.Reusable
+
+@Module(includes = [ContextModule::class, UserModule::class])
+internal abstract class ThemeModule {
+
+    @Binds
+    @Reusable
+    abstract fun bindServerThemeProvider(provider: ServerThemeProviderImpl): ServerThemeProvider
+
+    companion object {
+        @Provides
+        fun provideCurrentServerTheme(themeProvider: ServerThemeProvider): ServerTheme {
+            return themeProvider.getServerThemeForCurrentUser()
+        }
+    }
+}

+ 364 - 0
app/src/main/java/com/nextcloud/talk/ui/theme/ViewThemeUtils.kt

@@ -0,0 +1,364 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.ui.theme
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.graphics.PorterDuff
+import android.graphics.drawable.Drawable
+import android.view.View
+import android.widget.CheckBox
+import android.widget.EditText
+import android.widget.ImageView
+import android.widget.ProgressBar
+import android.widget.RadioButton
+import android.widget.SeekBar
+import android.widget.TextView
+import androidx.annotation.ColorInt
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.appcompat.widget.SwitchCompat
+import androidx.core.content.ContextCompat
+import androidx.core.content.res.ResourcesCompat
+import androidx.core.view.children
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.card.MaterialCardView
+import com.google.android.material.chip.Chip
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import com.google.android.material.progressindicator.LinearProgressIndicator
+import com.google.android.material.tabs.TabLayout
+import com.google.android.material.textfield.TextInputLayout
+import com.nextcloud.talk.R
+import com.nextcloud.talk.utils.DrawableUtils
+import com.nextcloud.talk.utils.ui.ColorUtil
+import com.nextcloud.talk.utils.ui.PlatformThemeUtil.isDarkMode
+import com.yarolegovich.mp.MaterialPreferenceCategory
+import com.yarolegovich.mp.MaterialSwitchPreference
+import javax.inject.Inject
+
+@Suppress("TooManyFunctions")
+class ViewThemeUtils @Inject constructor(private val theme: ServerTheme, private val colorUtil: ColorUtil) {
+
+    /**
+     * Color for painting elements
+     */
+    fun getElementColor(context: Context): Int = when {
+        isDarkMode(context) -> theme.colorElementDark
+        else -> theme.colorElementBright
+    }
+
+    private fun withElementColor(view: View, block: (Int) -> Unit) {
+        block(getElementColor(view.context))
+    }
+
+    fun themeFAB(fab: FloatingActionButton) {
+        withElementColor(fab) { color ->
+            fab.backgroundTintList = ColorStateList.valueOf(color)
+            fab.imageTintList = ColorStateList.valueOf(theme.colorText)
+        }
+    }
+
+    fun themeHorizontalSeekBar(seekBar: SeekBar) {
+        withElementColor(seekBar) { color ->
+            themeHorizontalSeekBar(seekBar, color)
+        }
+    }
+
+    fun themeHorizontalSeekBar(seekBar: SeekBar, @ColorInt color: Int) {
+        themeHorizontalProgressBar(seekBar, color)
+        seekBar.thumb.setColorFilter(color, PorterDuff.Mode.SRC_IN)
+    }
+
+    fun themeHorizontalProgressBar(progressBar: ProgressBar?, @ColorInt color: Int) {
+        if (progressBar != null) {
+            progressBar.indeterminateDrawable.setColorFilter(color, PorterDuff.Mode.SRC_IN)
+            progressBar.progressDrawable.setColorFilter(color, PorterDuff.Mode.SRC_IN)
+        }
+    }
+
+    fun colorTextViewElement(textView: TextView) {
+        withElementColor(textView) { color ->
+            textView.setTextColor(color)
+        }
+    }
+
+    fun colorTextViewText(textView: TextView) {
+        textView.setTextColor(theme.colorText)
+    }
+
+    /**
+     * Colors the background as element color and the foreground as text color.
+     */
+    fun colorImageViewButton(imageView: ImageView) {
+        withElementColor(imageView) { color ->
+            imageView.imageTintList = ColorStateList.valueOf(theme.colorText)
+            imageView.backgroundTintList = ColorStateList.valueOf(color)
+        }
+    }
+
+    /**
+     * Tints the image with element color
+     */
+    fun colorImageView(imageView: ImageView) {
+        withElementColor(imageView) { color ->
+            imageView.imageTintList = ColorStateList.valueOf(color)
+        }
+    }
+
+    /**
+     * Tints the image with text color
+     */
+    fun colorImageViewText(imageView: ImageView) {
+        imageView.imageTintList = ColorStateList.valueOf(theme.colorText)
+    }
+
+    fun colorMaterialButtonText(button: MaterialButton) {
+        withElementColor(button) { color ->
+            val disabledColor = ContextCompat.getColor(button.context, R.color.disabled_text)
+            val colorStateList = ColorStateList(
+                arrayOf(
+                    intArrayOf(android.R.attr.state_enabled),
+                    intArrayOf(-android.R.attr.state_enabled)
+                ),
+                intArrayOf(color, disabledColor)
+            )
+            button.setTextColor(colorStateList)
+            button.iconTint = colorStateList
+        }
+    }
+
+    fun colorMaterialButtonBackground(button: MaterialButton) {
+        withElementColor(button) { color ->
+            button.setBackgroundColor(color)
+
+            val disabledColor = ContextCompat.getColor(button.context, R.color.disabled_text)
+            val colorStateList = ColorStateList(
+                arrayOf(
+                    intArrayOf(android.R.attr.state_enabled),
+                    intArrayOf(-android.R.attr.state_enabled)
+                ),
+                intArrayOf(theme.colorText, disabledColor)
+            )
+
+            button.setTextColor(colorStateList)
+            button.iconTint = colorStateList
+        }
+    }
+
+    fun colorCardViewBackground(card: MaterialCardView) {
+        withElementColor(card) { color ->
+            card.setCardBackgroundColor(color)
+        }
+    }
+
+    // TODO split this util into classes depending on framework views vs library views
+    fun colorPreferenceCategory(category: MaterialPreferenceCategory) {
+        withElementColor(category) { color ->
+            category.setTitleColor(color)
+        }
+    }
+
+    fun colorSwitchPreference(preference: MaterialSwitchPreference) {
+        val children = preference.children
+        val switch = children.find { it is SwitchCompat }
+        if (switch != null) {
+            val switchCompat = (switch as SwitchCompat)
+            colorSwitchCompat(switchCompat)
+        }
+    }
+
+    fun colorSwitchCompat(switchCompat: SwitchCompat) {
+        withElementColor(switchCompat) { color ->
+
+            val context = switchCompat.context
+
+            val thumbUncheckedColor = ResourcesCompat.getColor(
+                context.resources,
+                R.color.switch_thumb_color_unchecked,
+                context.theme
+            )
+            val trackUncheckedColor = ResourcesCompat.getColor(
+                context.resources,
+                R.color.switch_track_color_unchecked,
+                context.theme
+            )
+
+            val trackColor =
+                Color.argb(SWITCHCOMPAT_TRACK_ALPHA, Color.red(color), Color.green(color), Color.blue(color))
+            switchCompat.thumbTintList = ColorStateList(
+                arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()),
+                intArrayOf(color, thumbUncheckedColor)
+            )
+
+            switchCompat.trackTintList = ColorStateList(
+                arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()),
+                intArrayOf(trackColor, trackUncheckedColor)
+            )
+        }
+    }
+
+    fun colorDrawable(context: Context, drawable: Drawable) {
+        val color = getElementColor(context)
+        drawable.setTint(color)
+    }
+
+    fun themeCheckbox(checkbox: CheckBox) {
+        withElementColor(checkbox) { color ->
+            checkbox.buttonTintList = ColorStateList(
+                arrayOf(
+                    intArrayOf(-android.R.attr.state_checked),
+                    intArrayOf(android.R.attr.state_checked)
+                ),
+                intArrayOf(Color.GRAY, color)
+            )
+        }
+    }
+
+    fun themeRadioButton(radioButton: RadioButton) {
+        withElementColor(radioButton) { color ->
+            radioButton.buttonTintList = ColorStateList(
+                arrayOf(
+                    intArrayOf(-android.R.attr.state_checked),
+                    intArrayOf(android.R.attr.state_checked)
+                ),
+                intArrayOf(Color.GRAY, color)
+            )
+        }
+    }
+
+    fun themeSwipeRefreshLayout(swipeRefreshLayout: SwipeRefreshLayout) {
+        withElementColor(swipeRefreshLayout) { color ->
+            swipeRefreshLayout.setColorSchemeColors(color)
+            swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.refresh_spinner_background)
+        }
+    }
+
+    fun colorProgressBar(progressIndicator: LinearProgressIndicator) {
+        withElementColor(progressIndicator) { color ->
+            progressIndicator.setIndicatorColor(progressColor(progressIndicator.context, color))
+        }
+    }
+
+    private fun progressColor(context: Context, color: Int): Int {
+        val lightness = when (isDarkMode(context)) {
+            true -> PROGRESS_LIGHTNESS_DARK_THEME
+            false -> PROGRESS_LIGHTNESS_LIGHT_THEME
+        }
+        return colorUtil.setLightness(color, lightness)
+    }
+
+    fun colorEditText(editText: EditText) {
+        withElementColor(editText) { color ->
+            editText.setTextColor(color)
+            // TODO check API-level compatibility
+            // editText.background.setColorFilter(color, PorterDuff.Mode.SRC_ATOP)
+            editText.backgroundTintList = ColorStateList(
+                arrayOf(
+                    intArrayOf(-android.R.attr.state_focused),
+                    intArrayOf(android.R.attr.state_focused)
+                ),
+                intArrayOf(
+                    Color.GRAY,
+                    color
+                )
+            )
+        }
+    }
+
+    fun colorTextInputLayout(textInputLayout: TextInputLayout) {
+        withElementColor(textInputLayout) { color ->
+            val errorColor = Color.GRAY
+
+            val errorColorStateList = ColorStateList(
+                arrayOf(
+                    intArrayOf(-android.R.attr.state_focused),
+                    intArrayOf(android.R.attr.state_focused)
+                ),
+                intArrayOf(
+                    errorColor,
+                    errorColor
+                )
+            )
+            val coloredColorStateList = ColorStateList(
+                arrayOf(
+                    intArrayOf(-android.R.attr.state_focused),
+                    intArrayOf(android.R.attr.state_focused)
+                ),
+                intArrayOf(
+                    Color.GRAY,
+                    color
+                )
+            )
+
+            textInputLayout.setBoxStrokeColorStateList(coloredColorStateList)
+            textInputLayout.setErrorIconTintList(errorColorStateList)
+            textInputLayout.setErrorTextColor(errorColorStateList)
+            textInputLayout.boxStrokeErrorColor = errorColorStateList
+            textInputLayout.defaultHintTextColor = coloredColorStateList
+        }
+    }
+
+    fun colorTabLayout(tabLayout: TabLayout) {
+        withElementColor(tabLayout) { color ->
+            tabLayout.setSelectedTabIndicatorColor(color)
+        }
+    }
+
+    fun getPlaceholderImage(context: Context, mimetype: String?): Drawable? {
+        val drawableResourceId = DrawableUtils.getDrawableResourceIdForMimeType(mimetype)
+        val drawable = AppCompatResources.getDrawable(
+            context,
+            drawableResourceId
+        )
+        if (drawable != null && THEMEABLE_PLACEHOLDER_IDS.contains(drawableResourceId)) {
+            colorDrawable(context, drawable)
+        }
+        return drawable
+    }
+
+    fun colorChipBackground(chip: Chip) {
+        withElementColor(chip) { color ->
+            chip.chipBackgroundColor = ColorStateList.valueOf(color)
+            chip.setTextColor(theme.colorText)
+        }
+    }
+
+    fun colorChipOutlined(chip: Chip, strokeWidth: Float) {
+        withElementColor(chip) { color ->
+            chip.chipBackgroundColor = ColorStateList.valueOf(Color.TRANSPARENT)
+            chip.chipStrokeWidth = strokeWidth
+            chip.chipStrokeColor = ColorStateList.valueOf(color)
+            chip.setTextColor(color)
+        }
+    }
+
+    companion object {
+        private val THEMEABLE_PLACEHOLDER_IDS = listOf(
+            R.drawable.ic_mimetype_package_x_generic,
+            R.drawable.ic_mimetype_folder
+        )
+        private const val SWITCHCOMPAT_TRACK_ALPHA: Int = 77
+        private const val PROGRESS_LIGHTNESS_LIGHT_THEME: Float = 0.76f
+        private const val PROGRESS_LIGHTNESS_DARK_THEME: Float = 0.28f
+    }
+}

+ 8 - 3
app/src/main/java/com/nextcloud/talk/users/UserManager.kt

@@ -28,23 +28,28 @@ import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.models.ExternalSignalingServer
 import com.nextcloud.talk.models.json.capabilities.Capabilities
 import com.nextcloud.talk.models.json.push.PushConfigurationState
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
 import io.reactivex.Maybe
+import io.reactivex.Observable
 import io.reactivex.Single
 
 @Suppress("TooManyFunctions")
-class UserManager internal constructor(private val userRepository: UsersRepository) : CurrentUserProviderNew {
+class UserManager internal constructor(private val userRepository: UsersRepository) {
     val users: Single<List<User>>
         get() = userRepository.getUsers()
 
     val usersScheduledForDeletion: Single<List<User>>
         get() = userRepository.getUsersScheduledForDeletion()
 
-    override val currentUser: Maybe<User>
+    val currentUser: Maybe<User>
         get() {
             return userRepository.getActiveUser()
         }
 
+    val currentUserObservable: Observable<User>
+        get() {
+            return userRepository.getActiveUserObservable()
+        }
+
     fun deleteUser(internalId: Long): Int {
         return userRepository.deleteUser(userRepository.getUserWithId(internalId).blockingGet())
     }

+ 16 - 26
app/src/main/java/com/nextcloud/talk/utils/DrawableUtils.kt

@@ -148,33 +148,23 @@ object DrawableUtils {
         drawableMap["unknown"] = R.drawable.ic_mimetype_file
         drawableMap["application/pdf"] = R.drawable.ic_mimetype_application_pdf
 
-        if (localMimetype.isNullOrEmpty()) {
-            return drawableMap["unknown"]!!
-        }
-
-        if ("DIR" == localMimetype) {
+        return if (localMimetype.isNullOrEmpty()) {
+            drawableMap["unknown"]!!
+        } else if ("DIR" == localMimetype) {
             localMimetype = FOLDER
-            return drawableMap[localMimetype]!!
-        }
-
-        if (drawableMap.containsKey(localMimetype)) {
-            return drawableMap[localMimetype]!!
-        }
-
-        if (localMimetype.startsWith(IMAGE_PREFIX)) {
-            return R.drawable.ic_mimetype_image
-        }
-
-        if (localMimetype.startsWith(VIDEO_PREFIX)) {
-            return R.drawable.ic_mimetype_video
-        }
-
-        if (localMimetype.startsWith(TEXT_PREFIX)) {
-            return R.drawable.ic_mimetype_text
-        }
-
-        return if (localMimetype.startsWith(AUDIO_PREFIX)) {
+            drawableMap[localMimetype]!!
+        } else if (drawableMap.containsKey(localMimetype)) {
+            drawableMap[localMimetype]!!
+        } else if (localMimetype.startsWith(IMAGE_PREFIX)) {
+            R.drawable.ic_mimetype_image
+        } else if (localMimetype.startsWith(VIDEO_PREFIX)) {
+            R.drawable.ic_mimetype_video
+        } else if (localMimetype.startsWith(TEXT_PREFIX)) {
+            R.drawable.ic_mimetype_text
+        } else if (localMimetype.startsWith(AUDIO_PREFIX)) {
             R.drawable.ic_mimetype_audio
-        } else drawableMap["unknown"]!!
+        } else {
+            drawableMap["unknown"]!!
+        }
     }
 }

+ 49 - 0
app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderImpl.kt

@@ -0,0 +1,49 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.utils.database.user
+
+import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.users.UserManager
+import io.reactivex.Maybe
+import io.reactivex.disposables.Disposable
+import javax.inject.Inject
+
+/**
+ * Listens to changes in the database and provides the current user without needing to query the database everytime.
+ */
+class CurrentUserProviderImpl @Inject constructor(private val userManager: UserManager) : CurrentUserProviderNew {
+    private var _currentUser: User? = null
+    private var currentUserObserver: Disposable? = null
+
+    override val currentUser: Maybe<User>
+        get() {
+            if (_currentUser == null) {
+                // immediately get a result synchronously
+                _currentUser = userManager.currentUser.blockingGet()
+                if (currentUserObserver == null) {
+                    // start observable for auto-updates
+                    currentUserObserver = userManager.currentUserObservable.subscribe { _currentUser = it }
+                }
+            }
+            return _currentUser?.let { Maybe.just(it) } ?: Maybe.empty()
+        }
+}

+ 1 - 1
app/src/main/java/com/nextcloud/talk/utils/database/user/UserModule.kt

@@ -30,7 +30,7 @@ import dagger.Provides
 abstract class UserModule {
 
     @Binds
-    abstract fun bindCurrentUserProviderNew(userManager: UserManager): CurrentUserProviderNew
+    abstract fun bindCurrentUserProviderNew(currentUserProviderImpl: CurrentUserProviderImpl): CurrentUserProviderNew
 
     companion object {
         @Provides

+ 81 - 0
app/src/main/java/com/nextcloud/talk/utils/ui/ColorUtil.kt

@@ -0,0 +1,81 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.utils.ui
+
+import android.content.Context
+import android.graphics.Color
+import androidx.annotation.ColorInt
+import androidx.annotation.ColorRes
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.ColorUtils
+import com.nextcloud.talk.R
+import javax.inject.Inject
+
+class ColorUtil @Inject constructor(private val context: Context) {
+
+    @ColorInt
+    fun getNullSafeColor(color: String?, @ColorInt fallbackColor: Int): Int {
+        return color.parseColorOrFallback { fallbackColor }
+    }
+
+    @ColorInt
+    fun getNullSafeColorWithFallbackRes(color: String?, @ColorRes fallbackColorRes: Int): Int {
+        return color.parseColorOrFallback { ContextCompat.getColor(context, fallbackColorRes) }
+    }
+
+    @ColorInt
+    fun getTextColor(colorText: String?, @ColorInt backgroundColor: Int): Int {
+        return colorText.parseColorOrFallback { getForegroundColorForBackgroundColor(backgroundColor) }
+    }
+
+    @ColorInt
+    fun getForegroundColorForBackgroundColor(@ColorInt color: Int): Int {
+        val hsl = FloatArray(HSL_SIZE)
+        ColorUtils.RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), hsl)
+
+        return if (hsl[INDEX_LIGHTNESS] < LIGHTNESS_DARK_THRESHOLD) {
+            Color.WHITE
+        } else {
+            ContextCompat.getColor(context, R.color.grey_900)
+        }
+    }
+
+    fun setLightness(@ColorInt color: Int, lightness: Float): Int {
+        require(lightness in 0.0..1.0) { "Lightness must be between 0 and 1" }
+        val hsl = FloatArray(HSL_SIZE)
+        ColorUtils.RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), hsl)
+
+        hsl[INDEX_LIGHTNESS] = lightness
+
+        return ColorUtils.HSLToColor(hsl)
+    }
+
+    @ColorInt
+    private fun String?.parseColorOrFallback(fallback: () -> Int): Int {
+        return this?.let { Color.parseColor(this) } ?: fallback()
+    }
+
+    companion object {
+        private const val HSL_SIZE: Int = 3
+        private const val INDEX_LIGHTNESS: Int = 2
+        private const val LIGHTNESS_DARK_THRESHOLD: Float = 0.6f
+    }
+}

+ 33 - 0
app/src/main/java/com/nextcloud/talk/utils/ui/PlatformThemeUtil.kt

@@ -0,0 +1,33 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.utils.ui
+
+import android.content.Context
+import android.content.res.Configuration
+
+object PlatformThemeUtil {
+    fun isDarkMode(context: Context): Boolean =
+        when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
+            Configuration.UI_MODE_NIGHT_YES -> true
+            else -> false
+        }
+}

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

@@ -222,6 +222,7 @@
         android:theme="@style/Button.Primary"
         android:tint="@android:color/white"
         android:visibility="gone"
+        tools:visibility="visible"
         app:backgroundTint="@color/colorPrimary"
         app:cornerRadius="48dp"
         app:elevation="0dp"

+ 2 - 0
app/src/main/res/layout/controller_conversation_info.xml

@@ -23,6 +23,7 @@
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:apc="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
+    tools:background="@color/white"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical">
@@ -76,6 +77,7 @@
                         android:layout_width="@dimen/avatar_size_big"
                         android:layout_height="@dimen/avatar_size_big"
                         android:layout_centerHorizontal="true"
+                        tools:background="@color/hwSecurityRed"
                         apc:roundAsCircle="true" />
 
                 </RelativeLayout>

+ 2 - 2
app/src/main/res/layout/controller_conversations_rv.xml

@@ -115,11 +115,11 @@
         android:layout_height="wrap_content"
         android:layout_gravity="bottom|end"
         android:layout_margin="16dp"
-        android:backgroundTint="@color/colorPrimary"
         android:contentDescription="@string/nc_new_conversation"
         app:borderWidth="0dp"
         app:srcCompat="@drawable/ic_add_white_24px"
-        app:tint="@color/white" />
+        app:tint="@color/white"
+        app:backgroundTint="@color/colorPrimary"/>
 
     <com.nextcloud.ui.popupbubble.PopupBubble
         android:id="@+id/newMentionPopupBubble"

+ 2 - 2
app/src/main/res/layout/controller_entry_menu.xml

@@ -63,7 +63,7 @@
             android:inputType="textUri"
             android:singleLine="true"
             android:textAlignment="viewStart"
-            android:textColor="@color/colorPrimary" />
+            android:textColor="@color/high_emphasis_text" />
 
     </com.google.android.material.textfield.TextInputLayout>
 
@@ -78,7 +78,7 @@
         android:contentDescription="@string/nc_add_emojis"
         android:src="@drawable/ic_insert_emoticon_black_24dp"
         android:visibility="gone"
-        app:tint="@color/emoji_icons"
+        app:tint="@color/medium_emphasis_text"
         tools:visibility="visible" />
 
 </RelativeLayout>

+ 2 - 1
app/src/main/res/layout/controller_profile.xml

@@ -29,7 +29,8 @@
     <RelativeLayout
         android:id="@+id/avatarContainer"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content">
+        android:layout_height="wrap_content"
+        android:paddingBottom="@dimen/standard_padding">
 
         <com.facebook.drawee.view.SimpleDraweeView
             android:id="@+id/avatar_image"

+ 7 - 1
app/src/main/res/layout/controller_settings.xml

@@ -27,6 +27,7 @@
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:fresco="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
+    tools:background="@color/white"
     android:id="@+id/settings_screen"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
@@ -43,6 +44,7 @@
             android:id="@+id/message_text"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
+            tools:text="This is a test message"
             android:gravity="center" />
     </com.yarolegovich.mp.MaterialPreferenceCategory>
 
@@ -105,10 +107,10 @@
                         android:id="@+id/server_age_warning_text"
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
+                        android:layout_centerHorizontal="true"
                         android:layout_toEndOf="@id/server_age_warning_icon"
                         android:paddingStart="@dimen/standard_padding"
                         android:paddingEnd="0dp"
-                        android:layout_centerHorizontal="true"
                         android:textAlignment="viewStart"
                         android:textColor="@color/nc_darkRed"
                         tools:text="@string/nc_settings_server_almost_eol" />
@@ -142,6 +144,7 @@
     <com.yarolegovich.mp.MaterialPreferenceCategory
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
+        android:id="@+id/settings_appearance_category"
         android:animateLayoutChanges="true"
         apc:cardBackgroundColor="@color/bg_default"
         apc:cardElevation="0dp"
@@ -186,6 +189,7 @@
     </com.yarolegovich.mp.MaterialPreferenceCategory>
 
     <com.yarolegovich.mp.MaterialPreferenceCategory
+        android:id="@+id/settings_privacy_category"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:animateLayoutChanges="true"
@@ -249,6 +253,7 @@
     </com.yarolegovich.mp.MaterialPreferenceCategory>
 
     <com.yarolegovich.mp.MaterialPreferenceCategory
+        android:id="@+id/settings_advanced_category"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:animateLayoutChanges="true"
@@ -323,6 +328,7 @@
     </com.yarolegovich.mp.MaterialPreferenceCategory>
 
     <com.yarolegovich.mp.MaterialPreferenceCategory
+        android:id="@+id/settings_about_category"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:animateLayoutChanges="true"

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

@@ -31,6 +31,7 @@
     app:cardElevation="0dp">
 
     <RelativeLayout
+        tools:background="@color/white"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:orientation="horizontal"

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

@@ -18,6 +18,7 @@
 <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"
+    tools:background="@color/white"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical">

+ 3 - 0
app/src/main/res/layout/dialog_poll_create.xml

@@ -35,6 +35,7 @@
         android:paddingTop="@dimen/dialog_padding_top_bottom">
 
         <TextView
+            android:id="@+id/poll_question"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:paddingStart="@dimen/dialog_padding"
@@ -73,6 +74,7 @@
         </com.google.android.material.textfield.TextInputLayout>
 
         <TextView
+            android:id="@+id/poll_options"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:paddingTop="@dimen/standard_padding"
@@ -102,6 +104,7 @@
             app:icon="@drawable/ic_add_grey600_24px" />
 
         <TextView
+            android:id="@+id/poll_settings"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_marginTop="@dimen/standard_margin"

+ 2 - 2
app/src/main/res/layout/item_custom_outcoming_location_message.xml

@@ -77,8 +77,8 @@
             android:layout_height="wrap_content"
             android:layout_below="@id/messageTime"
             android:layout_marginStart="8dp"
-            app:layout_alignSelf="center"
-            android:contentDescription="@null" />
+            android:contentDescription="@null"
+            app:layout_alignSelf="center" />
 
         <include
             android:id="@+id/reactions"

+ 4 - 4
app/src/main/res/layout/item_custom_outcoming_poll_message.xml

@@ -63,8 +63,8 @@
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:textAlignment="viewStart"
-                android:textStyle="bold"
                 android:textColor="@color/nc_outcoming_text_default"
+                android:textStyle="bold"
                 tools:text="This is the poll title?" />
 
         </LinearLayout>
@@ -83,8 +83,8 @@
             android:layout_height="wrap_content"
             android:layout_below="@id/messageText"
             android:layout_marginStart="8dp"
-            app:layout_alignSelf="center"
             android:textColor="@color/nc_outcoming_text_default"
+            app:layout_alignSelf="center"
             tools:text="10:35" />
 
         <ImageView
@@ -93,9 +93,9 @@
             android:layout_height="wrap_content"
             android:layout_below="@id/messageTime"
             android:layout_marginStart="8dp"
+            android:contentDescription="@null"
             android:textColor="@color/nc_outcoming_text_default"
-            app:layout_alignSelf="center"
-            android:contentDescription="@null" />
+            app:layout_alignSelf="center" />
 
         <include
             android:id="@+id/reactions"

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

@@ -27,6 +27,7 @@
     android:layout_height="wrap_content">
 
     <com.yarolegovich.mp.MaterialPreferenceCategory
+        android:id="@+id/notification_settings_category"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:animateLayoutChanges="true"

+ 33 - 21
app/src/main/res/layout/user_info_details_table_item.xml

@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?><!--
   Nextcloud Android client application
 
-  Copyright (C) 2018-2021 Andy Scherzinger
+  Copyright (C) 2018-2022 Andy Scherzinger
   Copyright (C) 2018 Nextcloud
 
   This program is free software; you can redistribute it and/or
@@ -22,8 +22,10 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/user_info_detail_container"
     android:layout_width="match_parent"
-    android:layout_height="@dimen/iconized_single_line_item_layout_height"
-    android:orientation="horizontal">
+    android:layout_height="wrap_content"
+    android:minHeight="@dimen/min_size_clickable_area"
+    android:orientation="horizontal"
+    android:paddingBottom="@dimen/standard_padding">
 
     <ImageView
         android:id="@+id/icon"
@@ -36,34 +38,44 @@
         app:layout_constraintTop_toTopOf="parent"
         tools:src="@drawable/ic_phone" />
 
-    <EditText
-        android:id="@+id/user_info_edit_text"
+    <com.google.android.material.textfield.TextInputLayout
+        android:id="@+id/user_info_input_layout"
+        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
         android:layout_width="0dp"
-        android:layout_height="48dp"
-        android:layout_marginStart="@dimen/standard_double_margin"
-        android:layout_marginEnd="@dimen/standard_margin"
-        android:autofillHints="none"
-        android:ellipsize="end"
-        android:inputType="text"
-        android:maxLines="1"
-        android:textSize="@dimen/two_line_primary_text_size"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/standard_margin"
+        android:minHeight="@dimen/min_size_clickable_area"
+        app:boxStrokeColor="@color/colorPrimary"
+        app:errorTextAppearance="@style/ErrorAppearance"
+        app:hintTextColor="@color/colorPrimary"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toStartOf="@id/scope"
         app:layout_constraintStart_toEndOf="@id/icon"
-        app:layout_constraintTop_toTopOf="parent"
-        tools:ignore="LabelFor"
-        tools:text="+49 123 456 789 12" />
+        app:layout_constraintTop_toTopOf="parent">
+
+        <com.nextcloud.talk.utils.EmojiTextInputEditText
+            android:id="@+id/user_info_edit_text_edit"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:imeOptions="actionNext"
+            android:inputType="text"
+            android:singleLine="true"
+            android:textAlignment="viewStart"
+            tools:text="+49 123 456 789 12" />
+
+    </com.google.android.material.textfield.TextInputLayout>
 
     <ImageView
         android:id="@+id/scope"
-        android:layout_width="48dp"
-        android:layout_height="48dp"
-        android:padding="12dp"
-        android:layout_marginEnd="4dp"
+        android:layout_width="@dimen/min_size_clickable_area"
+        android:layout_height="@dimen/min_size_clickable_area"
+        android:layout_marginStart="@dimen/standard_quarter_margin"
+        android:layout_marginEnd="@dimen/standard_quarter_margin"
         android:contentDescription="@string/scope_toggle"
+        android:padding="@dimen/scope_padding"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toEndOf="@id/user_info_edit_text"
+        app:layout_constraintStart_toEndOf="@id/user_info_input_layout"
         app:layout_constraintTop_toTopOf="parent"
         tools:src="@drawable/ic_link" />
 

+ 4 - 0
app/src/main/res/values-night/colors.xml

@@ -76,4 +76,8 @@
 
     <color name="dialog_background">#353535</color>
     <color name="vote_dialog_background">#424242</color>
+
+    <color name="switch_thumb_color_unchecked">#cbcbcb</color>
+    <color name="switch_track_color_unchecked">#5a5a5a</color>
+
 </resources>

+ 4 - 5
app/src/main/res/values/colors.xml

@@ -63,15 +63,13 @@
     <color name="nc_darkGreen">#006400</color>
     <color name="controller_chat_separator">#E8E8E8</color>
     <color name="grey_600">#757575</color>
+    <color name="grey_900">#212121</color>
     <color name="nc_grey">#D5D5D5</color>
     <color name="controller_call_incomingCallTextView">#E9FFFFFF</color>
     <color name="grey950">#111111</color>
     <color name="textColorMaxContrast">#767676</color>
     <color name="colorBackgroundDarker">#DBDBDB</color>
 
-    <!-- Emoji list in chat window -->
-    <color name="emoji_icons">#61000000</color>
-
     <color name="fg_default">#666666</color>
     <color name="fg_inverse">#FFFFFF</color>
 
@@ -82,8 +80,6 @@
 
     <color name="bg_message_list_incoming_bubble">#EFEFEF</color>
     <color name="bg_message_list_incoming_bubble_deleted">#66EFEFEF</color>
-    <color name="bg_message_list_outcoming_bubble">@color/colorPrimary</color>
-    <color name="bg_message_list_outcoming_bubble_deleted">#800082C9</color>
 
     <color name="bg_bottom_sheet">#FFFFFF</color>
     <color name="bg_call_screen_dialog">#121212</color>
@@ -111,4 +107,7 @@
     <color name="dialog_background">#FFFFFF</color>
     <color name="vote_dialog_background">#FFFFFF</color>
 
+    <color name="switch_thumb_color_unchecked">#ececec</color>
+    <color name="switch_track_color_unchecked">#b2b2b2</color>
+
 </resources>

+ 1 - 0
app/src/main/res/values/dimens.xml

@@ -72,6 +72,7 @@
     <dimen name="activity_row_layout_height">48dp</dimen>
     <dimen name="reaction_bottom_sheet_layout_size">40dp</dimen>
     <dimen name="standard_eighth_margin">2dp</dimen>
+    <dimen name="scope_padding">12dp</dimen>
 
     <dimen name="default_checkbox_dialog_start_margin">18dp</dimen>