Skip to content

Droid 3622 add counters to space chat widget #2449

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
May 23, 2025
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.bin.EmptyBin
import com.anytypeio.anytype.domain.block.interactor.Move
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.chats.ChatPreviewContainer
import com.anytypeio.anytype.domain.config.ConfigStorage
import com.anytypeio.anytype.domain.config.UserSettingsRepository
import com.anytypeio.anytype.domain.debugging.Logger
Expand Down Expand Up @@ -300,4 +301,5 @@ interface HomeScreenDependencies : ComponentDependencies {
fun featureToggles(): FeatureToggles
fun payloadDelegator(): PayloadDelegator
fun fieldParser(): FieldParser
fun chatPreviews(): ChatPreviewContainer
}
Original file line number Diff line number Diff line change
Expand Up @@ -229,15 +229,17 @@ object SubscriptionsModule {
isSpaceDeleted: SpaceDeletedStatusWatcher,
profileSubscriptionManager: ProfileSubscriptionManager,
networkConnectionStatus: NetworkConnectionStatus,
deviceTokenStoringService: DeviceTokenStoringService
deviceTokenStoringService: DeviceTokenStoringService,
chatPreviewContainer: ChatPreviewContainer
): GlobalSubscriptionManager = GlobalSubscriptionManager.Default(
types = types,
relations = relations,
permissions = permissions,
isSpaceDeleted = isSpaceDeleted,
profile = profileSubscriptionManager,
networkConnectionStatus = networkConnectionStatus,
deviceTokenStoringService = deviceTokenStoringService
deviceTokenStoringService = deviceTokenStoringService,
chatPreviewContainer = chatPreviewContainer
)

@JvmStatic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,9 @@ private fun WidgetList(
is WidgetView.SpaceChat -> {
SpaceChatWidgetCard(
mode = mode,
onWidgetClicked = { onBundledWidgetHeaderClicked(item.id) }
unReadMentionCount = item.unreadMentionCount,
unReadMessageCount = item.unreadMessageCount,
onWidgetClicked = { onWidgetSourceClicked(item.id, item.source) }
)
}
is WidgetView.Action.EditWidgets -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
Expand All @@ -17,19 +23,24 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.views.Caption1Regular
import com.anytypeio.anytype.core_ui.views.HeadlineSubheading
import com.anytypeio.anytype.presentation.home.InteractionMode

@Composable
fun SpaceChatWidgetCard(
mode: InteractionMode,
onWidgetClicked: () -> Unit = {}
onWidgetClicked: () -> Unit = {},
unReadMessageCount: Int = 0,
unReadMentionCount: Int = 0
) {
Box(
Row(
modifier = Modifier
.padding(start = 20.dp, end = 20.dp, top = 6.dp, bottom = 6.dp)
.fillMaxWidth()
Expand All @@ -47,45 +58,76 @@ fun SpaceChatWidgetCard(
} else {
Modifier
}
)
),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.ic_widget_all_content),
painter = painterResource(id = R.drawable.ic_widget_chat),
contentDescription = "All content icon",
modifier = Modifier
.align(Alignment.CenterStart)
.padding(start = 16.dp)
)

Text(
// Temporary hard-coded name for the widget
text = "Space chat",
text = stringResource(R.string.chat),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.align(Alignment.CenterStart)
.padding(start = 44.dp, end = 16.dp),
.weight(1f)
.padding(start = 8.dp, end = 16.dp),
style = HeadlineSubheading,
color = colorResource(id = R.color.text_primary),
)

if (unReadMentionCount > 0) {
Box(
modifier = Modifier
.background(
color = colorResource(R.color.transparent_active),
shape = CircleShape
)
.size(20.dp),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(R.drawable.ic_chat_widget_mention),
contentDescription = null
)
}
}

if (unReadMessageCount > 0) {
if (unReadMentionCount > 0) {
Spacer(modifier = Modifier.width(8.dp))
}
Box(
modifier = Modifier
.height(20.dp)
.defaultMinSize(minWidth = 20.dp)
.background(
color = colorResource(R.color.transparent_active),
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Text(
text = unReadMentionCount.toString(),
style = Caption1Regular,
color = colorResource(id = R.color.text_white),
)
}
Spacer(modifier = Modifier.width(16.dp))
}
}
}

@Preview(
name = "Dark Mode",
showBackground = true,
uiMode = UI_MODE_NIGHT_YES
)
@Preview(
name = "Light Mode",
showBackground = true,
uiMode = UI_MODE_NIGHT_NO
)
@DefaultPreviews
@Composable
fun SpaceChatWidgetPreview() {
SpaceChatWidgetCard(
onWidgetClicked = {},
mode = InteractionMode.Default
mode = InteractionMode.Default,
unReadMessageCount = 1,
unReadMentionCount = 1
)
}
9 changes: 9 additions & 0 deletions core-ui/src/main/res/drawable/ic_chat_widget_mention.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:viewportWidth="12"
android:viewportHeight="12">
<path
android:pathData="M6.366,11.887C5.409,11.887 4.564,11.756 3.83,11.494C3.095,11.234 2.478,10.851 1.978,10.345C1.478,9.839 1.1,9.219 0.844,8.484C0.588,7.75 0.459,6.909 0.459,5.962C0.459,5.047 0.589,4.228 0.849,3.506C1.111,2.784 1.491,2.172 1.988,1.669C2.488,1.162 3.094,0.776 3.806,0.511C4.522,0.245 5.331,0.112 6.234,0.112C7.113,0.112 7.881,0.256 8.541,0.544C9.203,0.828 9.756,1.214 10.2,1.701C10.647,2.186 10.981,2.731 11.203,3.337C11.428,3.944 11.541,4.569 11.541,5.212C11.541,5.665 11.519,6.125 11.475,6.59C11.431,7.056 11.339,7.484 11.198,7.875C11.058,8.262 10.841,8.575 10.547,8.812C10.256,9.05 9.863,9.169 9.366,9.169C9.147,9.169 8.906,9.134 8.644,9.065C8.381,8.997 8.149,8.883 7.945,8.723C7.742,8.564 7.622,8.35 7.584,8.081H7.528C7.453,8.262 7.338,8.434 7.181,8.597C7.028,8.759 6.827,8.889 6.577,8.986C6.33,9.083 6.028,9.125 5.672,9.112C5.266,9.097 4.908,9.006 4.599,8.84C4.289,8.672 4.03,8.444 3.82,8.156C3.614,7.865 3.458,7.529 3.352,7.148C3.249,6.764 3.197,6.35 3.197,5.906C3.197,5.484 3.259,5.098 3.384,4.748C3.509,4.398 3.683,4.092 3.905,3.829C4.13,3.567 4.392,3.358 4.692,3.201C4.995,3.042 5.322,2.944 5.672,2.906C5.984,2.875 6.269,2.889 6.525,2.948C6.781,3.004 6.992,3.09 7.158,3.206C7.324,3.319 7.428,3.444 7.472,3.581H7.528V3.056H8.522V7.294C8.522,7.556 8.595,7.787 8.742,7.987C8.889,8.187 9.103,8.287 9.384,8.287C9.703,8.287 9.947,8.178 10.116,7.959C10.288,7.74 10.405,7.403 10.467,6.947C10.533,6.49 10.566,5.906 10.566,5.194C10.566,4.775 10.508,4.362 10.392,3.956C10.28,3.547 10.108,3.164 9.877,2.808C9.649,2.451 9.359,2.137 9.009,1.865C8.659,1.594 8.249,1.381 7.777,1.228C7.308,1.072 6.775,0.994 6.178,0.994C5.444,0.994 4.786,1.108 4.205,1.336C3.627,1.561 3.134,1.89 2.728,2.325C2.325,2.756 2.017,3.281 1.805,3.9C1.595,4.515 1.491,5.215 1.491,6C1.491,6.797 1.595,7.504 1.805,8.123C2.017,8.742 2.33,9.264 2.742,9.689C3.158,10.114 3.672,10.436 4.284,10.655C4.897,10.876 5.603,10.987 6.403,10.987C6.747,10.987 7.086,10.955 7.42,10.889C7.755,10.823 8.05,10.751 8.306,10.673C8.563,10.595 8.747,10.537 8.859,10.5L9.122,11.362C8.928,11.444 8.675,11.525 8.363,11.606C8.053,11.687 7.722,11.755 7.369,11.808C7.019,11.861 6.684,11.887 6.366,11.887ZM5.822,8.156C6.241,8.156 6.58,8.072 6.839,7.903C7.099,7.734 7.288,7.479 7.406,7.139C7.525,6.798 7.584,6.369 7.584,5.85C7.584,5.325 7.519,4.915 7.388,4.622C7.256,4.328 7.063,4.122 6.806,4.003C6.55,3.884 6.234,3.825 5.859,3.825C5.503,3.825 5.199,3.919 4.945,4.106C4.695,4.29 4.503,4.537 4.369,4.847C4.238,5.153 4.172,5.487 4.172,5.85C4.172,6.25 4.225,6.626 4.331,6.979C4.438,7.329 4.611,7.614 4.852,7.833C5.092,8.048 5.416,8.156 5.822,8.156Z"
android:fillColor="@color/text_white"/>
</vector>
9 changes: 9 additions & 0 deletions core-ui/src/main/res/drawable/ic_widget_chat.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M6.961,20C5.532,20 6.175,19.491 6.485,19.087C6.786,18.683 6.762,18.637 7.206,17.894C7.27,17.773 7.23,17.651 7.111,17.587C4.563,16.197 3,13.928 3,11.343C3,7.28 7,4 12,4C17,4 21,7.28 21,11.343C21,15.365 17.206,18.677 11.484,18.677C11.381,18.677 11.278,18.669 11.175,18.661C11.064,18.661 10.952,18.701 10.818,18.798C9.468,19.579 7.913,20 6.961,20Z"
android:fillColor="@color/glyph_button"/>
</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.scan
import kotlinx.coroutines.launch

Expand All @@ -26,6 +28,7 @@ interface ChatPreviewContainer {

suspend fun getAll(): List<Chat.Preview>
suspend fun getPreview(space: SpaceId): Chat.Preview?
fun observePreview(space: SpaceId) : Flow<Chat.Preview?>
fun observePreviews() : Flow<List<Chat.Preview>>

class Default @Inject constructor(
Expand Down Expand Up @@ -113,6 +116,12 @@ interface ChatPreviewContainer {
return previews.value.firstOrNull { preview -> preview.space.id == space.id }
}

override fun observePreview(space: SpaceId): Flow<Chat.Preview?> {
return previews.map {
it.firstOrNull { preview -> preview.space.id == space.id }
}
}

override fun observePreviews(): Flow<List<Chat.Preview>> {
return previews
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.anytypeio.anytype.domain.subscriptions

import com.anytypeio.anytype.domain.chats.ChatPreviewContainer
import com.anytypeio.anytype.domain.device.DeviceTokenStoringService
import com.anytypeio.anytype.domain.device.NetworkConnectionStatus
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
Expand All @@ -21,7 +22,8 @@ interface GlobalSubscriptionManager {
private val isSpaceDeleted: SpaceDeletedStatusWatcher,
private val profile: ProfileSubscriptionManager,
private val networkConnectionStatus: NetworkConnectionStatus,
private val deviceTokenStoringService: DeviceTokenStoringService
private val deviceTokenStoringService: DeviceTokenStoringService,
private val chatPreviewContainer: ChatPreviewContainer
) : GlobalSubscriptionManager {

override fun onStart() {
Expand All @@ -32,6 +34,7 @@ interface GlobalSubscriptionManager {
profile.onStart()
networkConnectionStatus.start()
deviceTokenStoringService.start()
chatPreviewContainer.start()
}

override fun onStop() {
Expand All @@ -42,6 +45,7 @@ interface GlobalSubscriptionManager {
profile.onStop()
networkConnectionStatus.stop()
deviceTokenStoringService.stop()
chatPreviewContainer.stop()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ class ChatContainerTest {
listOf(
Event.Command.Chats.Delete(
context = givenChatID,
id = initialMsg.id,
message = initialMsg.id,
)
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ fun ChatTopToolbar(
.noRippleClickable { onSpaceNameClicked() }
,
text = when(header) {
is ChatViewModel.HeaderView.Default -> header.title
is ChatViewModel.HeaderView.Default -> header.title.ifEmpty {
stringResource(R.string.untitled)
}
is ChatViewModel.HeaderView.Init -> ""
},
color = colorResource(R.color.text_primary),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import com.anytypeio.anytype.domain.base.onSuccess
import com.anytypeio.anytype.domain.bin.EmptyBin
import com.anytypeio.anytype.domain.block.interactor.CreateBlock
import com.anytypeio.anytype.domain.block.interactor.Move
import com.anytypeio.anytype.domain.chats.ChatPreviewContainer
import com.anytypeio.anytype.domain.collections.AddObjectToCollection
import com.anytypeio.anytype.domain.dashboard.interactor.SetObjectListIsFavorite
import com.anytypeio.anytype.domain.dataview.interactor.CreateDataViewObject
Expand Down Expand Up @@ -122,6 +123,7 @@ import com.anytypeio.anytype.presentation.widgets.DropDownMenuAction
import com.anytypeio.anytype.presentation.widgets.LinkWidgetContainer
import com.anytypeio.anytype.presentation.widgets.ListWidgetContainer
import com.anytypeio.anytype.presentation.widgets.SpaceBinWidgetContainer
import com.anytypeio.anytype.presentation.widgets.SpaceChatWidgetContainer
import com.anytypeio.anytype.presentation.widgets.SpaceWidgetContainer
import com.anytypeio.anytype.presentation.widgets.TreePath
import com.anytypeio.anytype.presentation.widgets.TreeWidgetBranchStateHolder
Expand Down Expand Up @@ -233,7 +235,8 @@ class HomeScreenViewModel(
private val getSpaceInviteLink: GetSpaceInviteLink,
private val deleteSpace: DeleteSpace,
private val spaceMembers: ActiveSpaceMemberSubscriptionContainer,
private val setAsFavourite: SetObjectListIsFavorite
private val setAsFavourite: SetObjectListIsFavorite,
private val chatPreviews: ChatPreviewContainer
) : NavigationViewModel<HomeScreenViewModel.Navigation>(),
Reducer<ObjectView, Payload>,
WidgetActiveViewStateHolder by widgetActiveViewStateHolder,
Expand Down Expand Up @@ -507,6 +510,10 @@ class HomeScreenViewModel(

widgets.forceChatPosition().filter { widget -> widget.hasValidLayout() }.map { widget ->
when (widget) {
is Widget.Chat -> SpaceChatWidgetContainer(
widget = widget,
container = chatPreviews
)
is Widget.Link -> LinkWidgetContainer(
widget = widget,
fieldParser = fieldParser
Expand Down Expand Up @@ -1192,7 +1199,7 @@ class HomeScreenViewModel(
)
)
}
WidgetView.SpaceChat.id -> {
BundledWidgetSourceIds.CHAT -> {
proceedWithSpaceChatWidgetHeaderClick()
}
else -> {
Expand Down Expand Up @@ -1315,6 +1322,7 @@ class HomeScreenViewModel(
}
// All-objects widget has link appearance.
is Widget.AllObjects -> Command.ChangeWidgetType.TYPE_LINK
is Widget.Chat -> Command.ChangeWidgetType.TYPE_LINK
}

// TODO move to a separate reducer inject into this VM's constructor
Expand Down Expand Up @@ -2690,7 +2698,8 @@ class HomeScreenViewModel(
private val getSpaceInviteLink: GetSpaceInviteLink,
private val deleteSpace: DeleteSpace,
private val activeSpaceMemberSubscriptionContainer: ActiveSpaceMemberSubscriptionContainer,
private val setObjectListIsFavorite: SetObjectListIsFavorite
private val setObjectListIsFavorite: SetObjectListIsFavorite,
private val chatPreviews: ChatPreviewContainer
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = HomeScreenViewModel(
Expand Down Expand Up @@ -2748,7 +2757,8 @@ class HomeScreenViewModel(
getSpaceInviteLink = getSpaceInviteLink,
deleteSpace = [email protected],
spaceMembers = activeSpaceMemberSubscriptionContainer,
setAsFavourite = setObjectListIsFavorite
setAsFavourite = setObjectListIsFavorite,
chatPreviews = chatPreviews
) as T
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class DataViewListWidgetContainer(
)
)
}
is Widget.Link, is Widget.Tree, is Widget.AllObjects -> {
is Widget.Link, is Widget.Tree, is Widget.AllObjects, is Widget.Chat -> {
throw IllegalStateException("Incompatible widget type.")
}
}
Expand Down Expand Up @@ -150,7 +150,7 @@ class DataViewListWidgetContainer(
)
)
}
is Widget.Tree, is Widget.Link, is Widget.AllObjects -> {
is Widget.Tree, is Widget.Link, is Widget.AllObjects, is Widget.Chat -> {
throw IllegalStateException("Incompatible widget type.")
}
}
Expand Down Expand Up @@ -181,7 +181,7 @@ class DataViewListWidgetContainer(
limit = when (widget) {
is Widget.List -> widget.limit
is Widget.View -> widget.limit
is Widget.Tree, is Widget.Link, is Widget.AllObjects -> {
is Widget.Tree, is Widget.Link, is Widget.AllObjects, is Widget.Chat -> {
throw IllegalStateException("Incompatible widget type.")
}
}
Expand Down Expand Up @@ -355,7 +355,7 @@ class DataViewListWidgetContainer(
prettyPrintName = fieldParser.getObjectPluralName(widget.source.obj)
)
)
is Widget.Link, is Widget.Tree, is Widget.AllObjects -> {
is Widget.Link, is Widget.Tree, is Widget.AllObjects, is Widget.Chat -> {
throw IllegalStateException("Incompatible widget type.")
}
}
Expand Down
Loading