diff --git a/library/ui-styles/src/main/res/values-v23/dimens.xml b/library/ui-styles/src/main/res/values-v23/dimens.xml new file mode 100644 index 0000000000..18b8a81a7e --- /dev/null +++ b/library/ui-styles/src/main/res/values-v23/dimens.xml @@ -0,0 +1,4 @@ + + + 28dp + \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml index e2e50449ce..88c3d9d6e4 100644 --- a/library/ui-styles/src/main/res/values/dimens.xml +++ b/library/ui-styles/src/main/res/values/dimens.xml @@ -41,4 +41,8 @@ 320dp + 24dp + 48dp + 48dp + \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 1f5550b27f..907ca360bb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -680,17 +680,17 @@ class RoomDetailViewModel @AssistedInject constructor( } } else { when (itemId) { - R.id.timeline_setting -> true - R.id.invite -> state.canInvite - R.id.open_matrix_apps -> true - R.id.voice_call -> state.isWebRTCCallOptionAvailable() - R.id.video_call -> state.isWebRTCCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined + R.id.timeline_setting -> true + R.id.invite -> state.canInvite + R.id.open_matrix_apps -> true + R.id.voice_call -> state.isWebRTCCallOptionAvailable() + R.id.video_call -> state.isWebRTCCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined // Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^ - R.id.join_conference -> !state.isWebRTCCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined - R.id.search -> true - R.id.threads -> BuildConfig.THREADING_ENABLED - R.id.dev_tools -> vectorPreferences.developerMode() - else -> false + R.id.join_conference -> !state.isWebRTCCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined + R.id.search -> true + R.id.menu_timeline_thread_list -> BuildConfig.THREADING_ENABLED + R.id.dev_tools -> vectorPreferences.developerMode() + else -> false } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index a7db0af385..b37ba12f37 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -36,12 +36,14 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo +import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView import android.widget.Toast import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat import androidx.core.net.toUri import androidx.core.text.buildSpannedString import androidx.core.text.toSpannable @@ -61,6 +63,7 @@ import com.airbnb.epoxy.OnModelBuildFinishedListener import com.airbnb.epoxy.addGlidePreloader import com.airbnb.epoxy.glidePreloader import com.airbnb.mvrx.Mavericks +import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -133,6 +136,7 @@ import im.vector.app.features.command.Command import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.UnreadMessagesSharedViewModel import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.home.room.detail.composer.SendMode import im.vector.app.features.home.room.detail.composer.TextComposerAction @@ -285,6 +289,7 @@ class TimelineFragment @Inject constructor( private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel() private val textComposerViewModel: TextComposerViewModel by fragmentViewModel() + private val debouncer = Debouncer(createUIHandler()) private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback @@ -907,6 +912,13 @@ class TimelineFragment @Inject constructor( (joinConfItem.actionView as? JoinConferenceView)?.onJoinClicked = { roomDetailViewModel.handle(RoomDetailAction.JoinJitsiCall) } + + // Custom thread notification menu item + menu.findItem(R.id.menu_timeline_thread_list)?.let { menuItem -> + menuItem.actionView.setOnClickListener { + onOptionsItemSelected(menuItem) + } + } } override fun onPrepareOptionsMenu(menu: Menu) { @@ -946,6 +958,10 @@ class TimelineFragment @Inject constructor( actionView.findViewById(R.id.cart_badge).setTextOrHide("$widgetsCount") matrixAppsMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) } + + // Handle custom threads badge notification + updateMenuThreadNotificationBadge(menu, state) + } } @@ -971,7 +987,7 @@ class TimelineFragment @Inject constructor( callActionsHandler.onVideoCallClicked() true } - R.id.threads -> { + R.id.menu_timeline_thread_list -> { navigateToThreadList() true } @@ -1007,6 +1023,29 @@ class TimelineFragment @Inject constructor( } } + /** + * Update menu thread notification badge appropriately + */ + private fun updateMenuThreadNotificationBadge(menu: Menu, state: RoomDetailViewState) { + val menuThreadList = menu.findItem(R.id.menu_timeline_thread_list).actionView + val badgeFrameLayout = menuThreadList.findViewById(R.id.threadNotificationBadgeFrameLayout) + val badgeTextView = menuThreadList.findViewById(R.id.threadNotificationBadgeTextView) + + val unreadThreadMessages = 18 + state.pushCounter + + val userIsMentioned = true + if (unreadThreadMessages > 0) { + badgeFrameLayout.isVisible = true + badgeTextView.text = unreadThreadMessages.toString() + val badgeDrawable = DrawableCompat.wrap(badgeFrameLayout.background) + val color = ContextCompat.getColor(requireContext(), if (userIsMentioned) R.color.palette_vermilion else R.color.palette_gray_200) + DrawableCompat.setTint(badgeDrawable, color) + badgeFrameLayout.background = badgeDrawable + } else { + badgeFrameLayout.isVisible = false + } + } + /** * View and highlight the original root thread message in the main timeline */ diff --git a/vector/src/main/res/drawable/notification_badge.xml b/vector/src/main/res/drawable/notification_badge.xml new file mode 100644 index 0000000000..11f4b1d274 --- /dev/null +++ b/vector/src/main/res/drawable/notification_badge.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/view_thread_notification_badge.xml b/vector/src/main/res/layout/view_thread_notification_badge.xml new file mode 100644 index 0000000000..8e2e098d7b --- /dev/null +++ b/vector/src/main/res/layout/view_thread_notification_badge.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/view_thread_notification_badge_old.xml b/vector/src/main/res/layout/view_thread_notification_badge_old.xml new file mode 100644 index 0000000000..70efceda51 --- /dev/null +++ b/vector/src/main/res/layout/view_thread_notification_badge_old.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + diff --git a/vector/src/main/res/menu/menu_timeline.xml b/vector/src/main/res/menu/menu_timeline.xml index 532b63dd38..6b334ab56a 100644 --- a/vector/src/main/res/menu/menu_timeline.xml +++ b/vector/src/main/res/menu/menu_timeline.xml @@ -38,14 +38,13 @@ tools:visible="true" /> -