diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/settings/ProfileDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/settings/ProfileDI.kt index 8d831a3f89..dea0829c00 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/settings/ProfileDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/settings/ProfileDI.kt @@ -11,6 +11,7 @@ import com.anytypeio.anytype.domain.block.repo.BlockRepository import com.anytypeio.anytype.domain.config.ConfigStorage import com.anytypeio.anytype.domain.config.UserSettingsRepository import com.anytypeio.anytype.domain.debugging.DebugSpace +import com.anytypeio.anytype.domain.icon.RemoveObjectIcon import com.anytypeio.anytype.domain.icon.SetDocumentImageIcon import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer import com.anytypeio.anytype.domain.misc.UrlBuilder @@ -54,7 +55,8 @@ object ProfileModule { setDocumentImageIcon: SetDocumentImageIcon, membershipProvider: MembershipProvider, getNetworkMode: GetNetworkMode, - profileSubscriptionManager: ProfileSubscriptionManager + profileSubscriptionManager: ProfileSubscriptionManager, + removeObjectIcon: RemoveObjectIcon ): ProfileSettingsViewModel.Factory = ProfileSettingsViewModel.Factory( analytics = analytics, container = storelessSubscriptionContainer, @@ -64,7 +66,8 @@ object ProfileModule { setDocumentImageIcon = setDocumentImageIcon, membershipProvider = membershipProvider, getNetworkMode = getNetworkMode, - profileSubscriptionManager = profileSubscriptionManager + profileSubscriptionManager = profileSubscriptionManager, + removeObjectIcon = removeObjectIcon ) @Provides @@ -83,6 +86,13 @@ object ProfileModule { spaceManager = spaceManager ) + @Provides + @PerScreen + fun provideRemoveObjectIcon( + repo: BlockRepository, + dispatchers: AppCoroutineDispatchers + ): RemoveObjectIcon = RemoveObjectIcon(repo, dispatchers) + @JvmStatic @PerScreen @Provides diff --git a/app/src/main/java/com/anytypeio/anytype/ui/settings/ProfileSettingsFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/settings/ProfileSettingsFragment.kt index d728cb4c39..c8cd219d89 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/settings/ProfileSettingsFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/settings/ProfileSettingsFragment.kt @@ -119,7 +119,8 @@ class ProfileSettingsFragment : BaseBottomSheetComposeFragment() { findNavController().navigate(R.id.spaceListScreen) } } - ) + ), + clearProfileImage = { vm.onClearProfileImage() } ) } } diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/icon/RemoveObjectIcon.kt b/domain/src/main/java/com/anytypeio/anytype/domain/icon/RemoveObjectIcon.kt new file mode 100644 index 0000000000..204cd61057 --- /dev/null +++ b/domain/src/main/java/com/anytypeio/anytype/domain/icon/RemoveObjectIcon.kt @@ -0,0 +1,19 @@ +package com.anytypeio.anytype.domain.icon + +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.base.ResultInteractor +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.icon.RemoveObjectIcon.Params + +class RemoveObjectIcon( + private val repository: BlockRepository, + dispatchers: AppCoroutineDispatchers +): ResultInteractor(dispatchers.io) { + + override suspend fun doWork(params: Params) { + repository.removeDocumentIcon(ctx = params.objectId) + } + + data class Params(val objectId: Id) +} \ No newline at end of file diff --git a/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/account/ProfileScreen.kt b/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/account/ProfileScreen.kt index 6912383ae3..58c994cd76 100644 --- a/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/account/ProfileScreen.kt +++ b/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/account/ProfileScreen.kt @@ -1,5 +1,6 @@ package com.anytypeio.anytype.ui_settings.account +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -7,7 +8,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -20,6 +20,8 @@ import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -35,10 +37,10 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight @@ -46,10 +48,10 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.rememberAsyncImagePainter -import com.anytypeio.anytype.core_ui.foundation.Arrow import com.anytypeio.anytype.core_ui.foundation.Divider import com.anytypeio.anytype.core_ui.foundation.Dragger import com.anytypeio.anytype.core_ui.foundation.Option @@ -81,7 +83,8 @@ fun ProfileSettingsScreen( onSpacesClicked: () -> Unit, onMembershipClicked: () -> Unit, membershipStatus: MembershipStatus?, - showMembership: ShowMembership? + showMembership: ShowMembership?, + clearProfileImage: () -> Unit ) { LazyColumn( modifier = Modifier @@ -93,7 +96,8 @@ fun ProfileSettingsScreen( Header( account = account, onNameSet = onNameChange, - onProfileIconClick = onProfileIconClick + onProfileIconClick = onProfileIconClick, + clearProfileImage = clearProfileImage ) } item { @@ -272,7 +276,8 @@ private fun Header( modifier: Modifier = Modifier, account: AccountProfile, onProfileIconClick: () -> Unit, - onNameSet: (String) -> Unit + onNameSet: (String) -> Unit, + clearProfileImage: () -> Unit ) { when (account) { is AccountProfile.Data -> { @@ -286,7 +291,8 @@ private fun Header( ProfileImageBlock( name = account.name, icon = account.icon, - onProfileIconClick = onProfileIconClick + onProfileIconClick = onProfileIconClick, + clearProfileImage = clearProfileImage ) } ProfileNameBlock(name = account.name, onNameSet = onNameSet) @@ -396,8 +402,15 @@ fun ProfileTitleBlock() { fun ProfileImageBlock( name: String, icon: ProfileIconView, - onProfileIconClick: () -> Unit + onProfileIconClick: () -> Unit, + clearProfileImage: () -> Unit ) { + val isIconMenuExpanded = remember { + mutableStateOf(false) + } + + val context = LocalContext.current + when (icon) { is ProfileIconView.Image -> { Image( @@ -408,7 +421,7 @@ fun ProfileImageBlock( .size(96.dp) .clip(RoundedCornerShape(48.dp)) .noRippleClickable { - onProfileIconClick.invoke() + isIconMenuExpanded.value = !isIconMenuExpanded.value } ) } @@ -438,6 +451,48 @@ fun ProfileImageBlock( } } } + MaterialTheme( + shapes = MaterialTheme.shapes.copy(medium = RoundedCornerShape(16.dp)) + ) { + DropdownMenu( + expanded = isIconMenuExpanded.value, + offset = DpOffset(x = 0.dp, y = 6.dp), + onDismissRequest = { + isIconMenuExpanded.value = false + } + ) { + if (ActivityResultContracts.PickVisualMedia.isPhotoPickerAvailable(context)) { + androidx.compose.material.Divider( + thickness = 0.5.dp, + color = colorResource(id = R.color.shape_primary) + ) + DropdownMenuItem( + onClick = { + onProfileIconClick.invoke() + isIconMenuExpanded.value = false + }, + ) { + Text( + text = stringResource(R.string.profile_settings_apply_upload_image), + style = BodyRegular, + color = colorResource(id = R.color.text_primary) + ) + } + } + DropdownMenuItem( + onClick = { + isIconMenuExpanded.value = false + clearProfileImage.invoke() + }, + ) { + Text( + text = stringResource(R.string.profile_settings_remove_image), + style = BodyRegular, + color = colorResource(id = R.color.text_primary) + ) + } + } + } } @Preview @@ -459,7 +514,8 @@ private fun ProfileSettingPreview() { onSpacesClicked = {}, onMembershipClicked = {}, membershipStatus = null, - showMembership = ShowMembership(true) + showMembership = ShowMembership(true), + clearProfileImage = {} ) } diff --git a/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/account/ProfileSettingsViewModel.kt b/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/account/ProfileSettingsViewModel.kt index e47db809e3..05daf9efff 100644 --- a/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/account/ProfileSettingsViewModel.kt +++ b/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/account/ProfileSettingsViewModel.kt @@ -7,29 +7,23 @@ import androidx.lifecycle.viewModelScope import com.anytypeio.anytype.analytics.base.Analytics import com.anytypeio.anytype.analytics.base.EventsDictionary import com.anytypeio.anytype.analytics.base.sendEvent -import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.NetworkMode import com.anytypeio.anytype.core_models.Relations +import com.anytypeio.anytype.core_models.membership.MembershipStatus import com.anytypeio.anytype.core_models.primitives.SpaceId -import com.anytypeio.anytype.domain.account.DeleteAccount -import com.anytypeio.anytype.domain.base.BaseUseCase import com.anytypeio.anytype.domain.base.fold import com.anytypeio.anytype.domain.config.ConfigStorage +import com.anytypeio.anytype.domain.icon.RemoveObjectIcon import com.anytypeio.anytype.domain.icon.SetDocumentImageIcon import com.anytypeio.anytype.domain.icon.SetImageIcon -import com.anytypeio.anytype.domain.library.StoreSearchByIdsParams import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.networkmode.GetNetworkMode import com.anytypeio.anytype.domain.`object`.SetObjectDetails -import com.anytypeio.anytype.domain.search.PROFILE_SUBSCRIPTION_ID -import com.anytypeio.anytype.presentation.common.BaseViewModel -import com.anytypeio.anytype.presentation.extension.sendScreenSettingsDeleteEvent -import com.anytypeio.anytype.core_models.membership.MembershipStatus import com.anytypeio.anytype.domain.search.ProfileSubscriptionManager +import com.anytypeio.anytype.presentation.common.BaseViewModel import com.anytypeio.anytype.presentation.membership.provider.MembershipProvider import com.anytypeio.anytype.presentation.profile.AccountProfile -import com.anytypeio.anytype.presentation.profile.ProfileIconView import com.anytypeio.anytype.presentation.profile.profileIcon import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow @@ -48,7 +42,8 @@ class ProfileSettingsViewModel( private val setImageIcon: SetDocumentImageIcon, private val membershipProvider: MembershipProvider, private val getNetworkMode: GetNetworkMode, - private val profileContainer: ProfileSubscriptionManager + private val profileContainer: ProfileSubscriptionManager, + private val removeObjectIcon: RemoveObjectIcon ) : BaseViewModel() { private val jobs = mutableListOf() @@ -150,6 +145,27 @@ class ProfileSettingsViewModel( } } + fun onClearProfileImage() { + viewModelScope.launch { + val config = configStorage.getOrNull() + if (config != null) { + val params = RemoveObjectIcon.Params(objectId = config.profile) + removeObjectIcon.async( + params = params + ).fold( + onFailure = { + Timber.e("Error while removing profile image") + }, + onSuccess = { + // do nothing + } + ) + } else { + Timber.e("Missing config while trying to unset profile image") + } + } + } + class Factory( private val analytics: Analytics, private val container: StorelessSubscriptionContainer, @@ -159,7 +175,8 @@ class ProfileSettingsViewModel( private val setDocumentImageIcon: SetDocumentImageIcon, private val membershipProvider: MembershipProvider, private val getNetworkMode: GetNetworkMode, - private val profileSubscriptionManager: ProfileSubscriptionManager + private val profileSubscriptionManager: ProfileSubscriptionManager, + private val removeObjectIcon: RemoveObjectIcon ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { @@ -172,7 +189,8 @@ class ProfileSettingsViewModel( setImageIcon = setDocumentImageIcon, membershipProvider = membershipProvider, getNetworkMode = getNetworkMode, - profileContainer = profileSubscriptionManager + profileContainer = profileSubscriptionManager, + removeObjectIcon = removeObjectIcon ) as T } } diff --git a/localization/src/main/res/values/strings.xml b/localization/src/main/res/values/strings.xml index f1f9aa17ee..eec0d18594 100644 --- a/localization/src/main/res/values/strings.xml +++ b/localization/src/main/res/values/strings.xml @@ -61,6 +61,8 @@ Entry space Apply random solid color Upload image + Upload image + Remove image Delete space You can store up to %1$s of your files on our encrypted backup node for free. If you reach the limit, files will be stored only locally. In order to save space on your local device, you can offload all your files to our encrypted backup node. The files will be loaded back when you open them. diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt index 09571fb512..6e946b705c 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt @@ -642,7 +642,8 @@ class HomeScreenViewModel( .withLatestFrom(spaceManager.observe()) { dispatch, config -> when (dispatch) { is WidgetDispatchEvent.SourcePicked.Default -> { - if (dispatch.sourceLayout == ObjectType.Layout.DATE.code) { + if (dispatch.sourceLayout == ObjectType.Layout.DATE.code || + dispatch.sourceLayout == ObjectType.Layout.PARTICIPANT.code) { proceedWithCreatingWidget( ctx = config.widgets, source = dispatch.source, diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/SelectWidgetSourceViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/SelectWidgetSourceViewModel.kt index 9fd995ebc9..75b9853fed 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/SelectWidgetSourceViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/SelectWidgetSourceViewModel.kt @@ -208,7 +208,9 @@ class SelectWidgetSourceViewModel( isInEditMode = curr.isInEditMode ) } - if (view.layout == ObjectType.Layout.DATE) { + if (view.layout == ObjectType.Layout.DATE || + view.layout == ObjectType.Layout.PARTICIPANT + ) { isDismissed.value = true } }