diff --git a/app/gradle.properties b/app/gradle.properties index 691cab51a1..cfd365d301 100644 --- a/app/gradle.properties +++ b/app/gradle.properties @@ -1,4 +1,4 @@ version.versionMajor=0 version.versionMinor=38 -version.versionPatch=13 +version.versionPatch=21 version.useDatedVersionName=false \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/app/Notifications.kt b/app/src/main/java/com/anytypeio/anytype/app/Notifications.kt index 01626c1c89..891be2ca8b 100644 --- a/app/src/main/java/com/anytypeio/anytype/app/Notifications.kt +++ b/app/src/main/java/com/anytypeio/anytype/app/Notifications.kt @@ -99,6 +99,7 @@ class AnytypeNotificationService @Inject constructor( ) } is NotificationPayload.ParticipantRequestApproved -> { + Timber.d("Processing participant request approved notification : ${notification}") val placeholder = context.resources.getString(R.string.untitled) val title = context.resources.getString( R.string.multiplayer_notification_member_request_approved @@ -106,16 +107,26 @@ class AnytypeNotificationService @Inject constructor( val actionTitle = context.resources.getString( R.string.multiplayer_notification_go_to_space ) - val body = if (payload.permissions.isOwnerOrEditor()) { - context.resources.getString( - R.string.multiplayer_notification_member_request_approved_with_edit_rights, - payload.spaceName.ifEmpty { placeholder } - ) - } else { - context.resources.getString( - R.string.multiplayer_notification_member_request_approved_with_read_only_rights, - payload.spaceName.ifEmpty { placeholder } - ) + val permissions = payload.permissions + val body = when { + permissions == null -> { + context.resources.getString( + R.string.multiplayer_notification_member_request_approved_unknown_rights, + payload.spaceName.ifEmpty { placeholder } + ) + } + permissions.isOwnerOrEditor() -> { + context.resources.getString( + R.string.multiplayer_notification_member_request_approved_with_edit_rights, + payload.spaceName.ifEmpty { placeholder } + ) + } + else -> { + context.resources.getString( + R.string.multiplayer_notification_member_request_approved_with_read_only_rights, + payload.spaceName.ifEmpty { placeholder } + ) + } } val intent = Intent(context, MainActivity::class.java).apply { putExtra(Relations.SPACE_ID, payload.spaceId.id) diff --git a/app/src/main/java/com/anytypeio/anytype/device/NotificationBuilder.kt b/app/src/main/java/com/anytypeio/anytype/device/NotificationBuilder.kt deleted file mode 100644 index 189f8b998e..0000000000 --- a/app/src/main/java/com/anytypeio/anytype/device/NotificationBuilder.kt +++ /dev/null @@ -1,132 +0,0 @@ -package com.anytypeio.anytype.device - -import android.app.NotificationChannel -import android.app.NotificationChannelGroup -import android.app.NotificationManager -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.os.Build -import androidx.core.app.NotificationCompat -import com.anytypeio.anytype.R -import com.anytypeio.anytype.core_models.DecryptedPushContent -import com.anytypeio.anytype.core_models.Id -import com.anytypeio.anytype.core_models.Relations -import com.anytypeio.anytype.ui.main.MainActivity - -class NotificationBuilder( - private val context: Context, - private val notificationManager: NotificationManager -) { - - private val attachmentText get() = context.getString(R.string.attachment) - - private val createdChannels = mutableSetOf() - - fun buildAndNotify(message: DecryptedPushContent.Message, spaceId: Id) { - - // 1) Build the intent that'll open your MainActivity in the right chat - val pending = createChatPendingIntent( - context = context, - chatId = message.chatId, - spaceId = spaceId - ) - - // Format the notification body text - val bodyText = message.formatNotificationBody(attachmentText) - - // 2) put it all on one line: "Author: " - val singleLine = "${message.senderName.trim()}: $bodyText" - - val channelName = sanitizeChannelName(message.spaceName) - - createNotificationChannelIfNeeded( - channelId = spaceId, - channelName = channelName - ) - - val notif = NotificationCompat.Builder(context, spaceId) - .setSmallIcon(R.drawable.ic_app_notification) - .setContentTitle(message.spaceName.trim()) - .setContentText(singleLine) - .setStyle(NotificationCompat.BigTextStyle().bigText(singleLine)) - .setAutoCancel(true) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setContentIntent(pending) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setDefaults(NotificationCompat.DEFAULT_ALL) - .setCategory(NotificationCompat.CATEGORY_MESSAGE) - .setFullScreenIntent(pending, true) - .setLights(0xFF0000FF.toInt(), 300, 1000) - .setVibrate(longArrayOf(0, 500, 200, 500)) - .build() - - notificationManager.notify(System.currentTimeMillis().toInt(), notif) - } - - private fun createNotificationChannelIfNeeded( - channelId: String, - channelName: String - ) { - if (createdChannels.contains(channelId)) return - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel( - channelId, channelName, NotificationManager.IMPORTANCE_HIGH - ).apply { - description = "New messages notifications" - enableLights(true) - enableVibration(true) - setShowBadge(true) - lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC - group = CHANNEL_GROUP_ID - } - notificationManager.createNotificationChannel(channel) - createdChannels.add(channelId) - } - } - - /** - * Creates the tap-action intent and wraps it in a PendingIntent for notifications. - */ - fun createChatPendingIntent( - context: Context, - chatId: String, - spaceId: Id - ): PendingIntent { - // 1) Build the intent that'll open your MainActivity in the right chat - val intent = Intent(context, MainActivity::class.java).apply { - action = AnytypePushService.ACTION_OPEN_CHAT - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP - putExtra(Relations.CHAT_ID, chatId) - putExtra(Relations.SPACE_ID, spaceId) - } - - // 2) Wrap it in a one-shot immutable PendingIntent - return PendingIntent.getActivity( - context, - NOTIFICATION_REQUEST_CODE, - intent, - PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE - ) - } - - fun createChannelGroupIfNeeded() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val existingGroup = notificationManager.getNotificationChannelGroup(CHANNEL_GROUP_ID) - if (existingGroup == null) { - val group = NotificationChannelGroup(CHANNEL_GROUP_ID, CHANNEL_GROUP_NAME) - notificationManager.createNotificationChannelGroup(group) - } - } - } - - private fun sanitizeChannelName(name: String): String { - return name.trim().replace(Regex("[^a-zA-Z0-9 _-]"), "_") - } - - companion object { - private const val NOTIFICATION_REQUEST_CODE = 100 - private const val CHANNEL_GROUP_ID = "chats_group" - private const val CHANNEL_GROUP_NAME = "Chats" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/device/NotificationBuilderImpl.kt b/app/src/main/java/com/anytypeio/anytype/device/NotificationBuilderImpl.kt new file mode 100644 index 0000000000..21a5da9979 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/device/NotificationBuilderImpl.kt @@ -0,0 +1,169 @@ +package com.anytypeio.anytype.device + +import android.app.NotificationChannel +import android.app.NotificationChannelGroup +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.core.app.NotificationCompat +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.DecryptedPushContent +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.Relations +import com.anytypeio.anytype.domain.notifications.NotificationBuilder +import com.anytypeio.anytype.domain.resources.StringResourceProvider +import com.anytypeio.anytype.ui.main.MainActivity +import kotlin.math.absoluteValue +import timber.log.Timber + +class NotificationBuilderImpl( + private val context: Context, + private val notificationManager: NotificationManager, + private val resourceProvider: StringResourceProvider +) : NotificationBuilder { + + private val attachmentText get() = resourceProvider.getAttachmentText() + private val createdChannels = mutableSetOf() + + override fun buildAndNotify(message: DecryptedPushContent.Message, spaceId: Id) { + val channelId = "${spaceId}_${message.chatId}" + + ensureChannelExists( + channelId = channelId, + channelName = sanitizeChannelName(message.spaceName) + ) + + // Create pending intent to open chat + val pending = createChatPendingIntent( + context = context, + chatId = message.chatId, + spaceId = spaceId + ) + + // Format the notification body text + val bodyText = message.formatNotificationBody(attachmentText) + val singleLine = "${message.senderName.trim()}: $bodyText" + + val notif = NotificationCompat.Builder(context, channelId) + .setSmallIcon(R.drawable.ic_app_notification) + .setContentTitle(message.spaceName.trim()) + .setContentText(singleLine) + .setStyle(NotificationCompat.BigTextStyle().bigText(singleLine)) + .setAutoCancel(true) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setContentIntent(pending) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .setCategory(NotificationCompat.CATEGORY_MESSAGE) + .setFullScreenIntent(pending, true) + .setLights(0xFF0000FF.toInt(), 300, 1000) + .setVibrate(longArrayOf(0, 500, 200, 500)) + .build() + + // TODO maybe use message ID as notification ID? + notificationManager.notify(System.currentTimeMillis().toInt(), notif) + } + + /** + * Ensures the notification channel (and group) exist before notifying. + */ + private fun ensureChannelExists(channelId: String, channelName: String) { + createChannelGroupIfNeeded() + if (createdChannels.contains(channelId)) return + val channel = NotificationChannel( + channelId, + channelName, + NotificationManager.IMPORTANCE_HIGH + ).apply { + description = "New messages notifications" + enableLights(true) + enableVibration(true) + setShowBadge(true) + lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + group = CHANNEL_GROUP_ID + } + } + notificationManager.createNotificationChannel(channel) + createdChannels.add(channelId) + } + + /** + * Creates the tap-action intent and wraps it in a PendingIntent for notifications. + */ + private fun createChatPendingIntent( + context: Context, + chatId: String, + spaceId: Id + ): PendingIntent { + // 1) Build the intent that'll open your MainActivity in the right chat + val intent = Intent(context, MainActivity::class.java).apply { + action = AnytypePushService.ACTION_OPEN_CHAT + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + putExtra(Relations.CHAT_ID, chatId) + putExtra(Relations.SPACE_ID, spaceId) + } + + // A unique PendingIntent per chat target. + val requestCode = (chatId + spaceId).hashCode().absoluteValue + + // 2) Wrap it in a one-shot immutable PendingIntent + return PendingIntent.getActivity( + context, + requestCode, + intent, + PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE + ) + } + + fun createChannelGroupIfNeeded() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + try { + val existingGroup = + notificationManager.getNotificationChannelGroup(CHANNEL_GROUP_ID) + if (existingGroup == null) { + val group = NotificationChannelGroup(CHANNEL_GROUP_ID, CHANNEL_GROUP_NAME) + notificationManager.createNotificationChannelGroup(group) + } + } catch (e: NoSuchMethodError) { + Timber.e(e, "Error while creating or getting notification group") + // Some devices might not support getNotificationChannelGroup even on Android O + // Just create the group without checking if it exists + val group = NotificationChannelGroup(CHANNEL_GROUP_ID, CHANNEL_GROUP_NAME) + notificationManager.createNotificationChannelGroup(group) + } catch (e: Exception) { + Timber.e(e, "Error while creating or getting notification group") + val group = NotificationChannelGroup(CHANNEL_GROUP_ID, CHANNEL_GROUP_NAME) + notificationManager.createNotificationChannelGroup(group) + } + } + } + + /** + * Deletes notifications and the channel for a specific chat in a space, so that + * when the user opens that chat, old notifications are cleared. + */ + override fun clearNotificationChannel(spaceId: String, chatId: String) { + val channelId = "${spaceId}_${chatId}" + + // Remove posted notifications for this specific chat channel + notificationManager.activeNotifications + .filter { it.notification.channelId == channelId } + .forEach { notificationManager.cancel(it.id) } + + // Delete the specific chat channel + notificationManager.deleteNotificationChannel(channelId) + createdChannels.remove(channelId) + } + + private fun sanitizeChannelName(name: String): String { + return name.trim().replace(Regex("[^a-zA-Z0-9 _-]"), "_") + } + + companion object { + private const val CHANNEL_GROUP_ID = "chats_group" + private const val CHANNEL_GROUP_NAME = "Chats" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/device/PushMessageProcessor.kt b/app/src/main/java/com/anytypeio/anytype/device/PushMessageProcessor.kt index e688dec518..cec9077d72 100644 --- a/app/src/main/java/com/anytypeio/anytype/device/PushMessageProcessor.kt +++ b/app/src/main/java/com/anytypeio/anytype/device/PushMessageProcessor.kt @@ -1,6 +1,7 @@ package com.anytypeio.anytype.device import android.util.Base64 +import com.anytypeio.anytype.domain.notifications.NotificationBuilder import com.anytypeio.anytype.presentation.notifications.DecryptionPushContentService interface PushMessageProcessor { diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/MainEntryDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/MainEntryDI.kt index 956ced5904..5bdf4e1fba 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/MainEntryDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/MainEntryDI.kt @@ -1,7 +1,6 @@ package com.anytypeio.anytype.di.feature import com.anytypeio.anytype.analytics.base.Analytics -import com.anytypeio.anytype.core_utils.di.scope.PerDialog import com.anytypeio.anytype.core_utils.di.scope.PerScreen import com.anytypeio.anytype.domain.account.AccountStatusChannel import com.anytypeio.anytype.domain.account.AwaitAccountStartManager @@ -13,6 +12,7 @@ import com.anytypeio.anytype.domain.auth.repo.AuthRepository import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers import com.anytypeio.anytype.domain.config.ConfigStorage import com.anytypeio.anytype.domain.config.UserSettingsRepository +import com.anytypeio.anytype.domain.deeplink.PendingIntentStore import com.anytypeio.anytype.domain.device.PathProvider import com.anytypeio.anytype.domain.misc.LocaleProvider import com.anytypeio.anytype.domain.multiplayer.SpaceInviteResolver @@ -77,7 +77,8 @@ object MainEntryModule { globalSubscriptionManager: GlobalSubscriptionManager, spaceInviteResolver: SpaceInviteResolver, spaceManager: SpaceManager, - spaceViewSubscriptionContainer: SpaceViewSubscriptionContainer + spaceViewSubscriptionContainer: SpaceViewSubscriptionContainer, + pendingIntentStore: PendingIntentStore ): MainViewModelFactory = MainViewModelFactory( resumeAccount = resumeAccount, analytics = analytics, @@ -97,7 +98,8 @@ object MainEntryModule { globalSubscriptionManager = globalSubscriptionManager, spaceInviteResolver = spaceInviteResolver, spaceManager = spaceManager, - spaceViews = spaceViewSubscriptionContainer + spaceViews = spaceViewSubscriptionContainer, + pendingIntentStore = pendingIntentStore ) @JvmStatic diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectMenuDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectMenuDI.kt index 062355949d..f8b0ea0b5c 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectMenuDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectMenuDI.kt @@ -30,7 +30,6 @@ import com.anytypeio.anytype.domain.relations.RemoveFromFeaturedRelations import com.anytypeio.anytype.domain.templates.CreateTemplateFromObject import com.anytypeio.anytype.domain.widgets.CreateWidget import com.anytypeio.anytype.domain.workspace.SpaceManager -import com.anytypeio.anytype.other.DefaultDeepLinkResolver import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate import com.anytypeio.anytype.presentation.common.Action import com.anytypeio.anytype.presentation.common.Delegator @@ -223,11 +222,6 @@ object ObjectMenuModule { dispatchers = dispatchers ) - @JvmStatic - @Provides - @PerDialog - fun provideDeeplinkResolver() : DeepLinkResolver = DefaultDeepLinkResolver - @JvmStatic @Provides @PerDialog @@ -377,11 +371,6 @@ object ObjectSetMenuModule { dispatchers = dispatchers ) - @JvmStatic - @Provides - @PerDialog - fun provideDeeplinkResolver() : DeepLinkResolver = DefaultDeepLinkResolver - @JvmStatic @Provides @PerDialog diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/chats/ChatsDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/chats/ChatsDI.kt index 8ea4385221..0d93614809 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/chats/ChatsDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/chats/ChatsDI.kt @@ -12,10 +12,12 @@ import com.anytypeio.anytype.domain.block.repo.BlockRepository import com.anytypeio.anytype.domain.chats.ChatEventChannel import com.anytypeio.anytype.domain.config.UserSettingsRepository import com.anytypeio.anytype.domain.debugging.Logger +import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider +import com.anytypeio.anytype.domain.notifications.NotificationBuilder import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes import com.anytypeio.anytype.domain.spaces.ClearLastOpenedSpace import com.anytypeio.anytype.domain.workspace.SpaceManager @@ -101,4 +103,6 @@ interface ChatComponentDependencies : ComponentDependencies { fun context(): Context fun spaceManager(): SpaceManager fun notificationPermissionManager(): NotificationPermissionManager + fun storelessSubscriptionContainer(): StorelessSubscriptionContainer + fun notificationBuilder(): NotificationBuilder } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/notifications/PushDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/notifications/PushDI.kt index 09371b98b6..b9c525ba9f 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/notifications/PushDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/notifications/PushDI.kt @@ -1,15 +1,15 @@ package com.anytypeio.anytype.di.feature.notifications -import android.app.NotificationManager import android.content.Context import com.anytypeio.anytype.device.AnytypePushService import com.anytypeio.anytype.device.DefaultPushMessageProcessor -import com.anytypeio.anytype.device.NotificationBuilder import com.anytypeio.anytype.device.PushMessageProcessor import com.anytypeio.anytype.di.common.ComponentDependencies import com.anytypeio.anytype.di.main.ConfigModule.DEFAULT_APP_COROUTINE_SCOPE import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers import com.anytypeio.anytype.domain.device.DeviceTokenStoringService +import com.anytypeio.anytype.domain.notifications.NotificationBuilder +import com.anytypeio.anytype.domain.resources.StringResourceProvider import com.anytypeio.anytype.presentation.notifications.CryptoService import com.anytypeio.anytype.presentation.notifications.CryptoServiceImpl import com.anytypeio.anytype.presentation.notifications.DecryptionPushContentService @@ -38,25 +38,6 @@ interface PushContentComponent { @Module object PushContentModule { - @JvmStatic - @Provides - @Singleton - fun provideNotificationManager( - context: Context - ): NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - - @JvmStatic - @Provides - @Singleton - fun provideNotificationBuilder( - context: Context, - notificationManager: NotificationManager - ): NotificationBuilder = NotificationBuilder( - context = context, - notificationManager = notificationManager - ).apply { - createChannelGroupIfNeeded() - } @JvmStatic @Provides @@ -92,4 +73,6 @@ interface PushContentDependencies : ComponentDependencies { fun context(): Context @Named(DEFAULT_APP_COROUTINE_SCOPE) fun scope(): CoroutineScope fun dispatchers(): AppCoroutineDispatchers + fun provider(): StringResourceProvider + fun notificationBuilder(): NotificationBuilder } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/onboarding/signup/OnboardingMnemonicDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/onboarding/signup/OnboardingMnemonicDI.kt index ade1527b09..b6f5ab156b 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/onboarding/signup/OnboardingMnemonicDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/onboarding/signup/OnboardingMnemonicDI.kt @@ -6,6 +6,7 @@ import com.anytypeio.anytype.di.common.ComponentDependencies import com.anytypeio.anytype.domain.auth.interactor.GetMnemonic import com.anytypeio.anytype.domain.auth.repo.AuthRepository import com.anytypeio.anytype.domain.config.ConfigStorage +import com.anytypeio.anytype.domain.deeplink.PendingIntentStore import com.anytypeio.anytype.domain.device.NetworkConnectionStatus import com.anytypeio.anytype.domain.network.NetworkModeProvider import com.anytypeio.anytype.presentation.onboarding.signup.OnboardingMnemonicViewModel @@ -61,6 +62,7 @@ interface OnboardingMnemonicDependencies : ComponentDependencies { fun config(): ConfigStorage fun networkModeProvider(): NetworkModeProvider fun networkConnectionStatus(): NetworkConnectionStatus + fun pendingIntentStore(): PendingIntentStore } @Scope diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/onboarding/signup/OnboardingSoulCreationDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/onboarding/signup/OnboardingSoulCreationDI.kt index 8411126173..381fee815e 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/onboarding/signup/OnboardingSoulCreationDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/onboarding/signup/OnboardingSoulCreationDI.kt @@ -3,7 +3,6 @@ package com.anytypeio.anytype.di.feature.onboarding.signup import androidx.lifecycle.ViewModelProvider import com.anytypeio.anytype.CrashReporter import com.anytypeio.anytype.analytics.base.Analytics -import com.anytypeio.anytype.core_utils.di.scope.PerScreen import com.anytypeio.anytype.di.common.ComponentDependencies import com.anytypeio.anytype.domain.account.AwaitAccountStartManager import com.anytypeio.anytype.domain.auth.interactor.CreateAccount @@ -13,6 +12,7 @@ import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers 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.deeplink.PendingIntentStore import com.anytypeio.anytype.domain.device.PathProvider import com.anytypeio.anytype.domain.misc.LocaleProvider import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider @@ -131,6 +131,7 @@ interface OnboardingSoulCreationDependencies : ComponentDependencies { fun awaitAccountStartManager(): AwaitAccountStartManager fun globalSubscriptionManager(): GlobalSubscriptionManager fun stringResourceProvider(): StringResourceProvider + fun providePendingIntentStore(): PendingIntentStore } @Scope diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/vault/VaultDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/vault/VaultDI.kt index 99457479bb..8d41c7af91 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/vault/VaultDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/vault/VaultDI.kt @@ -2,7 +2,6 @@ package com.anytypeio.anytype.di.feature.vault import androidx.lifecycle.ViewModelProvider import com.anytypeio.anytype.analytics.base.Analytics -import com.anytypeio.anytype.core_utils.di.scope.PerDialog import com.anytypeio.anytype.core_utils.di.scope.PerScreen import com.anytypeio.anytype.di.common.ComponentDependencies import com.anytypeio.anytype.domain.account.AwaitAccountStartManager @@ -12,6 +11,7 @@ import com.anytypeio.anytype.domain.block.repo.BlockRepository import com.anytypeio.anytype.domain.chats.ChatPreviewContainer import com.anytypeio.anytype.domain.config.UserSettingsRepository import com.anytypeio.anytype.domain.debugging.Logger +import com.anytypeio.anytype.domain.deeplink.PendingIntentStore import com.anytypeio.anytype.domain.misc.AppActionManager import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.multiplayer.SpaceInviteResolver @@ -83,4 +83,5 @@ interface VaultComponentDependencies : ComponentDependencies { fun awaitAccount(): AwaitAccountStartManager fun profileContainer(): ProfileSubscriptionManager fun chatPreviewContainer(): ChatPreviewContainer + fun pendingIntentStore(): PendingIntentStore } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/main/NotificationsModule.kt b/app/src/main/java/com/anytypeio/anytype/di/main/NotificationsModule.kt index b963f5c73f..cd547ace04 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/main/NotificationsModule.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/main/NotificationsModule.kt @@ -1,5 +1,6 @@ package com.anytypeio.anytype.di.main +import android.app.NotificationManager import android.content.Context import android.content.SharedPreferences import com.anytypeio.anytype.app.AnytypeNotificationService @@ -7,10 +8,13 @@ import com.anytypeio.anytype.data.auth.event.NotificationsDateChannel import com.anytypeio.anytype.data.auth.event.NotificationsRemoteChannel import com.anytypeio.anytype.data.auth.event.PushKeyDataChannel import com.anytypeio.anytype.data.auth.event.PushKeyRemoteChannel +import com.anytypeio.anytype.device.NotificationBuilderImpl import com.anytypeio.anytype.domain.account.AwaitAccountStartManager import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers import com.anytypeio.anytype.domain.chats.PushKeyChannel +import com.anytypeio.anytype.domain.notifications.NotificationBuilder import com.anytypeio.anytype.domain.notifications.SystemNotificationService +import com.anytypeio.anytype.domain.resources.StringResourceProvider import com.anytypeio.anytype.domain.workspace.NotificationsChannel import com.anytypeio.anytype.middleware.EventProxy import com.anytypeio.anytype.middleware.interactor.EventHandlerChannel @@ -128,4 +132,24 @@ object NotificationsModule { context = context ) } + + @JvmStatic + @Provides + @Singleton + fun provideNotificationBuilder( + context: Context, + notificationManager: NotificationManager, + stringResourceProvider: StringResourceProvider + ): NotificationBuilder = NotificationBuilderImpl( + context = context, + notificationManager = notificationManager, + resourceProvider = stringResourceProvider + ) + + @JvmStatic + @Provides + @Singleton + fun provideNotificationManager( + context: Context + ): NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/main/SubscriptionsModule.kt b/app/src/main/java/com/anytypeio/anytype/di/main/SubscriptionsModule.kt index 89abdb5de1..1379e63b74 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/main/SubscriptionsModule.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/main/SubscriptionsModule.kt @@ -13,10 +13,12 @@ import com.anytypeio.anytype.domain.chats.ChatPreviewContainer import com.anytypeio.anytype.domain.config.ConfigStorage import com.anytypeio.anytype.domain.debugging.DebugAccountSelectTrace import com.anytypeio.anytype.domain.debugging.Logger +import com.anytypeio.anytype.domain.deeplink.PendingIntentStore import com.anytypeio.anytype.domain.device.NetworkConnectionStatus import com.anytypeio.anytype.domain.device.DeviceTokenStoringService import com.anytypeio.anytype.domain.event.interactor.SpaceSyncAndP2PStatusProvider import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer +import com.anytypeio.anytype.domain.misc.DeepLinkResolver import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.DefaultUserPermissionProvider import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer @@ -37,6 +39,7 @@ import com.anytypeio.anytype.domain.spaces.SpaceDeletedStatusWatcher import com.anytypeio.anytype.domain.subscriptions.GlobalSubscriptionManager import com.anytypeio.anytype.domain.workspace.SpaceManager import com.anytypeio.anytype.domain.workspace.SyncAndP2PStatusChannel +import com.anytypeio.anytype.other.DefaultDeepLinkResolver import com.anytypeio.anytype.presentation.sync.SpaceSyncAndP2PStatusProviderImpl import dagger.Module import dagger.Provides @@ -298,4 +301,14 @@ object SubscriptionsModule { dispatchers = dispatchers, scope = scope ) + + @JvmStatic + @Provides + @Singleton + fun provideDeeplinkResolver() : DeepLinkResolver = DefaultDeepLinkResolver + + @JvmStatic + @Provides + @Singleton + fun providePendingIntentStore(): PendingIntentStore = PendingIntentStore() } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/chats/ChatFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/chats/ChatFragment.kt index 031efe126c..7870862764 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/chats/ChatFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/chats/ChatFragment.kt @@ -321,6 +321,11 @@ class ChatFragment : BaseComposeFragment() { } } + override fun onResume() { + super.onResume() + vm.onResume() + } + // DI override fun injectDependencies() { diff --git a/app/src/main/java/com/anytypeio/anytype/ui/main/MainActivity.kt b/app/src/main/java/com/anytypeio/anytype/ui/main/MainActivity.kt index 0b32b4acc7..df35b12e83 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/main/MainActivity.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/main/MainActivity.kt @@ -455,7 +455,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main), AppNavigation.Pr intent.data?.let { uri -> val data = uri.toString() if (DefaultDeepLinkResolver.isDeepLink(data)) { - vm.onNewDeepLink(DefaultDeepLinkResolver.resolve(data)) + vm.handleNewDeepLink(DefaultDeepLinkResolver.resolve(data)) // Optionally clear to prevent repeat intent.action = null @@ -492,6 +492,8 @@ class MainActivity : AppCompatActivity(R.layout.activity_main), AppNavigation.Pr chatId = chatId, spaceId = spaceId ) + // Clearing from-notification-to-chat intent. + intent.replaceExtras(Bundle()) } else { // Do nothing, already there. } @@ -542,7 +544,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main), AppNavigation.Pr when { checkDeepLink && DefaultDeepLinkResolver.isDeepLink(raw) -> { - vm.onNewDeepLink(DefaultDeepLinkResolver.resolve(raw)) + vm.handleNewDeepLink(DefaultDeepLinkResolver.resolve(raw)) } raw.isNotEmpty() && !DefaultDeepLinkResolver.isDeepLink(raw) -> { vm.onIntentTextShare(raw) @@ -726,7 +728,6 @@ class MainActivity : AppCompatActivity(R.layout.activity_main), AppNavigation.Pr override fun onResume() { super.onResume() - NotificationManagerCompat.from(this).cancelAll() mdnsProvider.start() navigator.bind(findNavController(R.id.fragment)) } diff --git a/app/src/main/java/com/anytypeio/anytype/ui/multiplayer/RequestJoinSpaceFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/multiplayer/RequestJoinSpaceFragment.kt index e57bb41181..391cb62af6 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/multiplayer/RequestJoinSpaceFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/multiplayer/RequestJoinSpaceFragment.kt @@ -72,7 +72,9 @@ class RequestJoinSpaceFragment : BaseBottomSheetComposeFragment() { return ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { - val bottomSheetState = rememberModalBottomSheetState() + val bottomSheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true + ) val scope = rememberCoroutineScope() val launcher = rememberLauncherForActivityResult( diff --git a/app/src/main/java/com/anytypeio/anytype/ui/multiplayer/ShareSpaceFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/multiplayer/ShareSpaceFragment.kt index 1f378f67b2..030c390e53 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/multiplayer/ShareSpaceFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/multiplayer/ShareSpaceFragment.kt @@ -145,7 +145,7 @@ class ShareSpaceFragment : BaseBottomSheetComposeFragment() { } dialog.show(childFragmentManager, null) }.onFailure { - Timber.e(it, "Error while navigation") + Timber.e(it, "Error while showing remove member warning") } } is Command.ShowStopSharingWarning -> { diff --git a/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/OnboardingAuthScreen.kt b/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/OnboardingAuthScreen.kt index edf32f2848..289f724d6a 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/OnboardingAuthScreen.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/OnboardingAuthScreen.kt @@ -36,7 +36,6 @@ import com.anytypeio.anytype.core_ui.OnBoardingTextSecondaryColor import com.anytypeio.anytype.core_ui.OnboardingSubtitleColor import com.anytypeio.anytype.core_ui.views.ButtonSize import com.anytypeio.anytype.core_ui.views.HeadlineOnBoardingDescription -import com.anytypeio.anytype.core_ui.views.HeadlineOnBoardingTitle import com.anytypeio.anytype.core_ui.views.OnBoardingButtonPrimary import com.anytypeio.anytype.core_ui.views.OnBoardingButtonSecondary import com.anytypeio.anytype.core_ui.views.TextOnBoardingDescription diff --git a/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/signup/OnboardingMnemonicPhraseScreen.kt b/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/signup/OnboardingMnemonicPhraseScreen.kt index 98df37835b..e7c996a984 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/signup/OnboardingMnemonicPhraseScreen.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/signup/OnboardingMnemonicPhraseScreen.kt @@ -78,7 +78,7 @@ fun MnemonicPhraseScreenWrapper( copyMnemonicToClipboard = copyMnemonicToClipboard, mnemonicColorPalette = mnemonicColorPalette, onGoToAppClicked = { - vm.onGoToTheAppClicked( + vm.handleAppEntryClick( space = space, startingObject = startingObject ) diff --git a/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultFragment.kt index 4c8d246127..e3f50ef113 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultFragment.kt @@ -14,7 +14,6 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavOptions import androidx.navigation.fragment.findNavController -import com.anytypeio.anytype.BuildConfig.USE_EDGE_TO_EDGE import com.anytypeio.anytype.R import com.anytypeio.anytype.core_utils.ext.argOrNull import com.anytypeio.anytype.core_utils.ext.toast @@ -245,6 +244,7 @@ class VaultFragment : BaseComposeFragment() { override fun onResume() { super.onResume() proceedWithDeepLinks() + vm.processPendingDeeplink() } private fun proceedWithDeepLinks() { diff --git a/app/src/test/java/com/anytypeio/anytype/device/DefaultPushMessageProcessorTest.kt b/app/src/test/java/com/anytypeio/anytype/device/DefaultPushMessageProcessorTest.kt index 426073af52..3dd1099dbd 100644 --- a/app/src/test/java/com/anytypeio/anytype/device/DefaultPushMessageProcessorTest.kt +++ b/app/src/test/java/com/anytypeio/anytype/device/DefaultPushMessageProcessorTest.kt @@ -3,6 +3,7 @@ package com.anytypeio.anytype.device import android.os.Build import android.util.Base64 import com.anytypeio.anytype.core_models.DecryptedPushContent +import com.anytypeio.anytype.domain.notifications.NotificationBuilder import com.anytypeio.anytype.presentation.notifications.DecryptionPushContentService import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue diff --git a/app/src/test/java/com/anytypeio/anytype/device/NotificationBuilderTest.kt b/app/src/test/java/com/anytypeio/anytype/device/NotificationBuilderTest.kt new file mode 100644 index 0000000000..42f6295bef --- /dev/null +++ b/app/src/test/java/com/anytypeio/anytype/device/NotificationBuilderTest.kt @@ -0,0 +1,220 @@ +package com.anytypeio.anytype.device + +import android.app.Notification +import android.app.NotificationManager +import android.content.Context +import android.os.Build +import android.service.notification.StatusBarNotification +import androidx.test.core.app.ApplicationProvider +import com.anytypeio.anytype.core_models.DecryptedPushContent +import com.anytypeio.anytype.domain.resources.StringResourceProvider +import kotlin.test.Test +import org.junit.After +import org.junit.Before +import org.junit.runner.RunWith +import org.mockito.Mockito.clearInvocations +import org.mockito.kotlin.any +import org.mockito.kotlin.argThat +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Build.VERSION_CODES.P]) // API 28 +class NotificationBuilderTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + lateinit var notificationManager: NotificationManager + lateinit var stringResourceProvider: StringResourceProvider + private lateinit var builder: NotificationBuilderImpl + private val testSpaceId = "space123" + private val testChatId = "chat456" + + // A simple stub for DecryptedPushContent.Message + private val message = DecryptedPushContent.Message( + chatId = testChatId, + senderName = "Alice", + spaceName = "My Space", + msgId = "msg789", + text = "Hello, this is a test message.", + hasAttachments = false + ) + + @Before + fun setUp() { + stringResourceProvider = mock { + on { getAttachmentText() } doReturn "[attachment]" + } + notificationManager = mock() + builder = NotificationBuilderImpl(context, notificationManager, stringResourceProvider) + } + + @After + fun tearDown() { + clearInvocations(notificationManager) + } + + @Test + fun `buildAndNotify should create channel and post notification`() { + // When + builder.buildAndNotify(message, testSpaceId) + + // Then: a channel should be created with correct id and name + verify(notificationManager).createNotificationChannel(argThat { + id == "${testSpaceId}_${testChatId}" && name == "My Space" + }) + // And a notification should be posted + verify(notificationManager).notify(any(), any()) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.O]) // API 26+ for channels + fun `clearNotificationChannel should cancel active and delete channel`() { + val channelId = "${testSpaceId}_${testChatId}" + // Prepare two mock StatusBarNotifications + val notif1: android.app.Notification = mock() + val notif2: android.app.Notification = mock() + + whenever(notif1.channelId).thenReturn(channelId) + whenever(notif2.channelId).thenReturn("other") + + // Wrap them in StatusBarNotification mocks + val sbn1: StatusBarNotification = mock() + val sbn2: StatusBarNotification = mock() + + whenever(sbn1.notification).thenReturn(notif1) + whenever(sbn1.id).thenReturn(1) + whenever(sbn2.notification).thenReturn(notif2) + whenever(sbn2.id).thenReturn(2) + + whenever(notificationManager.activeNotifications).thenReturn(arrayOf(sbn1, sbn2)) + + // Ensure channel exists + builder.buildAndNotify(message, testSpaceId) + + // When + builder.clearNotificationChannel(testSpaceId, testChatId) + + // Then active notifications for this channel are cancelled + verify(notificationManager).cancel(sbn1.id) + // Other channels remain + verify(notificationManager, never()).cancel(sbn2.id) + // And channel is deleted + verify(notificationManager).deleteNotificationChannel(channelId) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.O]) // API 26+ for channels + fun `clearNotificationChannel with multiple chats in same space should only clear specified chat`() { + // Prepare three mock notifications for different chats in the same space + val chat1 = "chat1" + val chat2 = "chat2" + val chat3 = "chat3" + val notif1: Notification = mock { + on { channelId } doReturn "${testSpaceId}_${chat1}" + } + val notif2: Notification = mock { + on { channelId } doReturn "${testSpaceId}_${chat2}" + } + val notif3: Notification = mock { + on { channelId } doReturn "${testSpaceId}_${chat3}" + } + val sbn1: StatusBarNotification = mock { + on { notification } doReturn notif1 + on { id } doReturn 10 + } + val sbn2: StatusBarNotification = mock { + on { notification } doReturn notif2 + on { id } doReturn 20 + } + val sbn3: StatusBarNotification = mock { + on { notification } doReturn notif3 + on { id } doReturn 30 + } + whenever(notificationManager.activeNotifications).thenReturn(arrayOf(sbn1, sbn2, sbn3)) + + // Ensure channels exist by sending a dummy notification for each chat + builder.buildAndNotify(message.copy(chatId = chat1), testSpaceId) + builder.buildAndNotify(message.copy(chatId = chat2), testSpaceId) + builder.buildAndNotify(message.copy(chatId = chat3), testSpaceId) + + // Clear only chat2 + builder.clearNotificationChannel(testSpaceId, chat2) + + // Verify only notifications for chat2 were cancelled + verify(notificationManager, never()).cancel(10) + verify(notificationManager).cancel(20) + verify(notificationManager, never()).cancel(30) + // Verify only the specified channel was deleted + verify(notificationManager).deleteNotificationChannel("${testSpaceId}_${chat2}") + verify(notificationManager, never()).deleteNotificationChannel("${testSpaceId}_${chat1}") + verify(notificationManager, never()).deleteNotificationChannel("${testSpaceId}_${chat3}") + } + + @Test + @Config(sdk = [Build.VERSION_CODES.O]) // API 26+ for channels + fun `clearNotificationChannel with multiple spaces and chats should only clear specified chat`() { + // Prepare notifications for different spaces and chats + val space1 = "space1" + val space2 = "space2" + val chat1 = "chat1" + val chat2 = "chat2" + + val notif1: Notification = mock { + on { channelId } doReturn "${space1}_${chat1}" + } + val notif2: Notification = mock { + on { channelId } doReturn "${space1}_${chat2}" + } + val notif3: Notification = mock { + on { channelId } doReturn "${space2}_${chat1}" + } + val notif4: Notification = mock { + on { channelId } doReturn "${space2}_${chat2}" + } + + val sbn1: StatusBarNotification = mock { + on { notification } doReturn notif1 + on { id } doReturn 10 + } + val sbn2: StatusBarNotification = mock { + on { notification } doReturn notif2 + on { id } doReturn 20 + } + val sbn3: StatusBarNotification = mock { + on { notification } doReturn notif3 + on { id } doReturn 30 + } + val sbn4: StatusBarNotification = mock { + on { notification } doReturn notif4 + on { id } doReturn 40 + } + + whenever(notificationManager.activeNotifications).thenReturn(arrayOf(sbn1, sbn2, sbn3, sbn4)) + + // Ensure channels exist by sending a dummy notification for each + builder.buildAndNotify(message.copy(chatId = chat1), space1) + builder.buildAndNotify(message.copy(chatId = chat2), space1) + builder.buildAndNotify(message.copy(chatId = chat1), space2) + builder.buildAndNotify(message.copy(chatId = chat2), space2) + + // Clear only space1_chat2 + builder.clearNotificationChannel(space1, chat2) + + // Verify only notifications for space1_chat2 were cancelled + verify(notificationManager, never()).cancel(10) + verify(notificationManager).cancel(20) + verify(notificationManager, never()).cancel(30) + verify(notificationManager, never()).cancel(40) + + // Verify only the specified channel was deleted + verify(notificationManager).deleteNotificationChannel("${space1}_${chat2}") + verify(notificationManager, never()).deleteNotificationChannel("${space1}_${chat1}") + verify(notificationManager, never()).deleteNotificationChannel("${space2}_${chat1}") + verify(notificationManager, never()).deleteNotificationChannel("${space2}_${chat2}") + } +} diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/Notification.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/Notification.kt index 3590bf343c..1e68eff56d 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/Notification.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/Notification.kt @@ -10,7 +10,7 @@ data class Notification( val isLocal: Boolean, val payload: NotificationPayload, val space: SpaceId, - val aclHeadId: String + val aclHeadId: String? = null ) { sealed class Event { @@ -52,7 +52,7 @@ sealed class NotificationPayload { data class ParticipantRequestApproved( val spaceId: SpaceId, val spaceName: String, - val permissions: SpaceMemberPermissions + val permissions: SpaceMemberPermissions? = null ) : NotificationPayload() diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectWrapper.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectWrapper.kt index de284cf09e..a1762c2d79 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectWrapper.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectWrapper.kt @@ -200,7 +200,7 @@ sealed class ObjectWrapper { } } - val defaultTemplateId: Id? by default + val defaultTemplateId: Id? get() = getSingleValue(Relations.DEFAULT_TEMPLATE_ID) val restrictions: List get() = when (val value = map[Relations.RESTRICTIONS]) { diff --git a/core-models/src/test/java/com/anytypeio/anytype/core_models/ObjectWrapperTypeDefaultTemplateIdTest.kt b/core-models/src/test/java/com/anytypeio/anytype/core_models/ObjectWrapperTypeDefaultTemplateIdTest.kt new file mode 100644 index 0000000000..0d8fed2bfd --- /dev/null +++ b/core-models/src/test/java/com/anytypeio/anytype/core_models/ObjectWrapperTypeDefaultTemplateIdTest.kt @@ -0,0 +1,144 @@ +package com.anytypeio.anytype.core_models + +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import java.util.UUID + +/** + * Tests for DROID-3763 fix: ClassCastException in ObjectWrapper.Type.defaultTemplateId + * + * This test class specifically addresses the crash that occurred when the backend + * returned a List for defaultTemplateId field instead of a single String value. + * + * Original error: java.util.Collections$UnmodifiableRandomAccessList cannot be cast to java.lang.String + */ +class ObjectWrapperTypeDefaultTemplateIdTest { + + @Test + fun `should not crash when defaultTemplateId is provided as UnmodifiableList`() { + val templateId = UUID.randomUUID().toString() + + // This simulates the exact crash scenario where backend returns an UnmodifiableList + val unmodifiableList = java.util.Collections.unmodifiableList(listOf(templateId)) + + val objectType = ObjectWrapper.Type( + map = mapOf( + Relations.DEFAULT_TEMPLATE_ID to unmodifiableList + ) + ) + + // This should not throw ClassCastException anymore + assertEquals(templateId, objectType.defaultTemplateId) + } + + @Test + fun `should handle ArrayList for defaultTemplateId`() { + val templateId = UUID.randomUUID().toString() + + val objectType = ObjectWrapper.Type( + map = mapOf( + Relations.DEFAULT_TEMPLATE_ID to arrayListOf(templateId) + ) + ) + + assertEquals(templateId, objectType.defaultTemplateId) + } + + @Test + fun `should handle mutableList for defaultTemplateId`() { + val templateId = UUID.randomUUID().toString() + + val objectType = ObjectWrapper.Type( + map = mapOf( + Relations.DEFAULT_TEMPLATE_ID to mutableListOf(templateId) + ) + ) + + assertEquals(templateId, objectType.defaultTemplateId) + } + + @Test + fun `should handle single string value for defaultTemplateId`() { + val templateId = UUID.randomUUID().toString() + + val objectType = ObjectWrapper.Type( + map = mapOf( + Relations.DEFAULT_TEMPLATE_ID to templateId + ) + ) + + assertEquals(templateId, objectType.defaultTemplateId) + } + + @Test + fun `should return first element from multi-item list for defaultTemplateId`() { + val firstTemplateId = UUID.randomUUID().toString() + val secondTemplateId = UUID.randomUUID().toString() + + val objectType = ObjectWrapper.Type( + map = mapOf( + Relations.DEFAULT_TEMPLATE_ID to listOf(firstTemplateId, secondTemplateId) + ) + ) + + assertEquals(firstTemplateId, objectType.defaultTemplateId) + } + + @Test + fun `should return null for empty list defaultTemplateId`() { + val objectType = ObjectWrapper.Type( + map = mapOf( + Relations.DEFAULT_TEMPLATE_ID to emptyList() + ) + ) + + assertNull(objectType.defaultTemplateId) + } + + @Test + fun `should return null for null defaultTemplateId`() { + val objectType = ObjectWrapper.Type( + map = mapOf( + Relations.DEFAULT_TEMPLATE_ID to null + ) + ) + + assertNull(objectType.defaultTemplateId) + } + + @Test + fun `should return null when defaultTemplateId is missing from map`() { + val objectType = ObjectWrapper.Type( + map = mapOf( + Relations.ID to UUID.randomUUID().toString(), + Relations.NAME to "Test Type" + ) + ) + + assertNull(objectType.defaultTemplateId) + } + + @Test + fun `should work correctly in mapper scenario that caused original crash`() { + val templateId = UUID.randomUUID().toString() + val typeId = UUID.randomUUID().toString() + val typeName = "Test Object Type" + + // Simulate the exact data structure that caused the crash + val objectType = ObjectWrapper.Type( + map = mapOf( + Relations.ID to typeId, + Relations.NAME to typeName, + Relations.UNIQUE_KEY to "testKey", + Relations.DEFAULT_TEMPLATE_ID to java.util.Collections.unmodifiableList(listOf(templateId)) + ) + ) + + // This should work without throwing ClassCastException + // This is the exact line from MapperExtension.kt that was crashing + val defaultTemplate = objectType.defaultTemplateId + + assertEquals(templateId, defaultTemplate) + } +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/views/OnBoardingTypographyCompose.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/views/OnBoardingTypographyCompose.kt index 8a52c9f561..e40f665935 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/views/OnBoardingTypographyCompose.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/views/OnBoardingTypographyCompose.kt @@ -3,24 +3,16 @@ package com.anytypeio.anytype.core_ui.views import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontLoadingStrategy import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.em import androidx.compose.ui.unit.sp import com.anytypeio.anytype.core_ui.R val fontRiccioneRegular = FontFamily( - Font(R.font.riccione_regular, weight = FontWeight.Normal) + Font(R.font.riccione_regular, weight = FontWeight.Normal, loadingStrategy = FontLoadingStrategy.Async) ) -val HeadlineOnBoardingTitle = - TextStyle( - fontFamily = fontRiccioneRegular, - fontWeight = FontWeight.W500, - fontSize = 60.sp, - lineHeight = 60.sp, - letterSpacing = (-0.05).em - ) - val HeadlineOnBoardingDescription = TextStyle( fontFamily = fontInterRegular, diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/views/TypographyCompose.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/views/TypographyCompose.kt index 4c3067a46c..1295db15df 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/views/TypographyCompose.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/views/TypographyCompose.kt @@ -5,28 +5,29 @@ import androidx.compose.ui.text.font.Font import androidx.compose.ui.unit.sp import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontLoadingStrategy import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.em import com.anytypeio.anytype.core_ui.R val fontInterRegular = FontFamily( - Font(R.font.inter_regular, weight = FontWeight.Normal) + Font(R.font.inter_regular, weight = FontWeight.Normal, loadingStrategy = FontLoadingStrategy.Async) ) val fontInterMedium = FontFamily( - Font(R.font.inter_medium, weight = FontWeight.Medium) + Font(R.font.inter_medium, weight = FontWeight.Medium, loadingStrategy = FontLoadingStrategy.Async) ) val fontInterBold = FontFamily( - Font(R.font.inter_bold, weight = FontWeight.Bold) + Font(R.font.inter_bold, weight = FontWeight.Bold, loadingStrategy = FontLoadingStrategy.Async) ) val fontInterSemibold = FontFamily( - Font(R.font.inter_semibold, weight = FontWeight.SemiBold) + Font(R.font.inter_semibold, weight = FontWeight.SemiBold, loadingStrategy = FontLoadingStrategy.Async) ) val fontIBM = FontFamily( - Font(R.font.ibm_plex_mono, weight = FontWeight.Normal) + Font(R.font.ibm_plex_mono, weight = FontWeight.Normal, loadingStrategy = FontLoadingStrategy.Async) ) //Content/Headlines/Title diff --git a/crowdin.yml b/crowdin.yml index 3bab572b21..7578c1224b 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -16,6 +16,7 @@ export_languages: - 'nl' - 'be' - 'tr' + - 'ja' files: - source: /localization/src/main/res/values/strings.xml translation: /localization/src/main/res/values-%android_code%/strings.xml diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/chats/ChatContainer.kt b/domain/src/main/java/com/anytypeio/anytype/domain/chats/ChatContainer.kt index d3632b27c9..f38d41b04b 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/chats/ChatContainer.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/chats/ChatContainer.kt @@ -11,15 +11,21 @@ import com.anytypeio.anytype.core_models.chats.Chat import com.anytypeio.anytype.core_models.primitives.Space import com.anytypeio.anytype.domain.block.repo.BlockRepository import com.anytypeio.anytype.domain.debugging.Logger +import com.anytypeio.anytype.domain.library.StoreSearchByIdsParams +import com.anytypeio.anytype.domain.library.StoreSearchParams +import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer import javax.inject.Inject import kotlin.collections.isNotEmpty import kotlin.collections.toList +import kotlin.math.log +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge @@ -29,7 +35,8 @@ import kotlinx.coroutines.flow.scan class ChatContainer @Inject constructor( private val repo: BlockRepository, private val channel: ChatEventChannel, - private val logger: Logger + private val logger: Logger, + private val subscription: StorelessSubscriptionContainer ) { private val lastMessages = LinkedHashMap() @@ -40,36 +47,28 @@ class ChatContainer @Inject constructor( private val attachments = MutableStateFlow>(emptySet()) private val replies = MutableStateFlow>(emptySet()) - // TODO Naive implementation. Add caching logic - fun fetchAttachments(space: Space) : Flow> { + @OptIn(ExperimentalCoroutinesApi::class) + fun subscribeToAttachments(chat: Id, space: Space) : Flow> { return attachments - .map { ids -> - if (ids.isNotEmpty()) { - repo.searchObjects( - sorts = emptyList(), - limit = 0, - filters = buildList { - DVFilter( - relation = Relations.ID, - value = ids.toList(), - condition = DVFilterCondition.IN - ) - }, - keys = emptyList(), - space = space - ).mapNotNull { - val wrapper = ObjectWrapper.Basic(it) - if (wrapper.isValid) wrapper else null - } - } else { - emptyList() + .flatMapLatest { ids -> + subscription.subscribe( + searchParams = StoreSearchByIdsParams( + subscription = "$chat/$ATTACHMENT_SUBSCRIPTION_POSTFIX", + space = space, + targets = ids.toList(), + keys = ATTACHMENT_KEYS + ) + ).map { wrappers -> + wrappers.associateBy { it.id } + } + } + .catch { e -> + emit(emptyMap()).also { + logger.logException(e, "DROID-2966 Error in the chat attachments pub/sub flow") } } - .distinctUntilChanged() - .map { wrappers -> wrappers.associateBy { it.id } } } - // TODO Naive implementation. Add caching logic fun fetchReplies(chat: Id) : Flow> { return replies .map { ids -> @@ -85,7 +84,12 @@ class ChatContainer @Inject constructor( } } .distinctUntilChanged() - .map { messages -> messages.associate { it.id to it } } + .map { messages -> messages.associateBy { it.id } } + .catch { e -> + emit(emptyMap()).also { + logger.logException(e, "DROID-2966 Error while fetching chat replies") + } + } } fun watchWhileTrackingAttachments(chat: Id): Flow { @@ -105,6 +109,19 @@ class ChatContainer @Inject constructor( } } + suspend fun stop(chat: Id) { + runCatching { + repo.unsubscribeChat(chat) + repo.cancelObjectSearchSubscription( + listOf("$chat/$ATTACHMENT_SUBSCRIPTION_POSTFIX") + ) + }.onFailure { + logger.logWarning("DROID-2966 Error while unsubscribing from chat") + }.onSuccess { + logger.logInfo("DROID-2966 Successfully unsubscribed from chat") + } + } + fun watch(chat: Id): Flow = flow { val response = repo.subscribeLastChatMessages( command = Command.ChatCommand.SubscribeLastMessages( @@ -119,7 +136,7 @@ class ChatContainer @Inject constructor( var intent: Intent = Intent.None - val initial = buildList { + val initial = buildList { if (initialState.hasUnReadMessages && !initialState.oldestMessageOrderId.isNullOrEmpty()) { // Starting from the unread-messages window. val aroundUnread = loadAroundMessageOrder( @@ -210,7 +227,6 @@ class ChatContainer @Inject constructor( logger.logException(e, "DROID-2966 Error while loading reply context") state.messages } - val target = messages.find { it.order == oldestReadOrderId } ChatStreamState( messages = messages, intent = Intent.ScrollToBottom, @@ -296,7 +312,7 @@ class ChatContainer @Inject constructor( ) ) }.onFailure { - logger.logException(it, "DROID-2966 Error while reading mentions") + logger.logWarning("DROID-2966 Error while reading mentions: ${it.message}") }.onSuccess { logger.logInfo("DROID-2966 Read mentions with success") } @@ -336,7 +352,7 @@ class ChatContainer @Inject constructor( ) ) }.onFailure { - logger.logException(it, "DROID-2966 Error while reading messages") + logger.logWarning("DROID-2966 Error while reading messages: ${it.message}") }.onSuccess { logger.logInfo("DROID-2966 Read messages with success") } @@ -665,6 +681,30 @@ class ChatContainer @Inject constructor( // TODO reduce message size to reduce UI and VM overload. private const val MAX_CHAT_CACHE_SIZE = 1000 private const val LAST_MESSAGES_MAX_SIZE = 10 + private const val ATTACHMENT_SUBSCRIPTION_POSTFIX = "attachments" + + + private val ATTACHMENT_KEYS = listOf( + Relations.ID, + Relations.SPACE_ID, + Relations.PICTURE, + Relations.SOURCE, + Relations.DESCRIPTION, + Relations.NAME, + Relations.ICON_IMAGE, + Relations.ICON_EMOJI, + Relations.ICON_NAME, + Relations.ICON_OPTION, + Relations.TYPE, + Relations.LAYOUT, + Relations.IS_ARCHIVED, + Relations.IS_DELETED, + Relations.DONE, + Relations.SNIPPET, + Relations.SIZE_IN_BYTES, + Relations.FILE_MIME_TYPE, + Relations.FILE_EXT, + ) } data class ChatMessageMeta(val id: Id, val order: String) diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/chats/ChatPreviewContainer.kt b/domain/src/main/java/com/anytypeio/anytype/domain/chats/ChatPreviewContainer.kt index 597242ea6c..df0a5ebcbe 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/chats/ChatPreviewContainer.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/chats/ChatPreviewContainer.kt @@ -48,7 +48,7 @@ interface ChatPreviewContainer { job = scope.launch(dispatchers.io) { previews.value = emptyList() val initial = runCatching { repo.subscribeToMessagePreviews(SUBSCRIPTION_ID) } - .onFailure { logger.logException(it, "DROID-2966 Error while getting initial previews") } + .onFailure { logger.logWarning("DROID-2966 Error while getting initial previews: ${it.message}") } .getOrDefault(emptyList()) events .subscribe(SUBSCRIPTION_ID) diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/deeplink/PendingIntentStore.kt b/domain/src/main/java/com/anytypeio/anytype/domain/deeplink/PendingIntentStore.kt new file mode 100644 index 0000000000..18f7423ae4 --- /dev/null +++ b/domain/src/main/java/com/anytypeio/anytype/domain/deeplink/PendingIntentStore.kt @@ -0,0 +1,23 @@ +package com.anytypeio.anytype.domain.deeplink + +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Store for keeping pending invite deeplinks in memory. + * This is used to handle invite deeplinks that were received while user was not logged in. + */ +@Singleton +class PendingIntentStore @Inject constructor() { + private var deepLinkInvite: String? = null + + fun setDeepLinkInvite(link: String?) { + deepLinkInvite = link + } + + fun getDeepLinkInvite(): String? = deepLinkInvite + + fun clearDeepLinkInvite() { + deepLinkInvite = null + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/notifications/NotificationBuilder.kt b/domain/src/main/java/com/anytypeio/anytype/domain/notifications/NotificationBuilder.kt new file mode 100644 index 0000000000..0de5566c22 --- /dev/null +++ b/domain/src/main/java/com/anytypeio/anytype/domain/notifications/NotificationBuilder.kt @@ -0,0 +1,9 @@ +package com.anytypeio.anytype.domain.notifications + +import com.anytypeio.anytype.core_models.DecryptedPushContent +import com.anytypeio.anytype.core_models.Id + +interface NotificationBuilder { + fun buildAndNotify(message: DecryptedPushContent.Message, spaceId: Id) + fun clearNotificationChannel(spaceId: String, chatId: String) +} \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/resources/StringResourceProvider.kt b/domain/src/main/java/com/anytypeio/anytype/domain/resources/StringResourceProvider.kt index de87cb36f7..e5090dc3c8 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/resources/StringResourceProvider.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/resources/StringResourceProvider.kt @@ -10,4 +10,5 @@ interface StringResourceProvider { fun getSetOfObjectsTitle(): String fun getPropertiesFormatPrettyString(format: RelationFormat): String fun getDefaultSpaceName(): String + fun getAttachmentText(): String } \ No newline at end of file diff --git a/domain/src/test/java/com/anytypeio/anytype/domain/chats/ChatContainerTest.kt b/domain/src/test/java/com/anytypeio/anytype/domain/chats/ChatContainerTest.kt index a726ee4365..3a62181392 100644 --- a/domain/src/test/java/com/anytypeio/anytype/domain/chats/ChatContainerTest.kt +++ b/domain/src/test/java/com/anytypeio/anytype/domain/chats/ChatContainerTest.kt @@ -11,6 +11,7 @@ import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers import com.anytypeio.anytype.domain.block.repo.BlockRepository import com.anytypeio.anytype.domain.common.DefaultCoroutineTestRule import com.anytypeio.anytype.domain.debugging.Logger +import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer import com.anytypeio.anytype.test_utils.MockDataFactory import kotlin.test.assertEquals import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -47,6 +48,9 @@ class ChatContainerTest { @Mock lateinit var logger: Logger + @Mock + lateinit var storelessSubscriptionContainer: StorelessSubscriptionContainer + private val givenChatID = MockDataFactory.randomUuid() @Before @@ -61,7 +65,8 @@ class ChatContainerTest { val container = ChatContainer( repo = repo, channel = channel, - logger = logger + logger = logger, + subscription = storelessSubscriptionContainer ) val msg = StubChatMessage( @@ -125,7 +130,8 @@ class ChatContainerTest { val container = ChatContainer( repo = repo, channel = channel, - logger = logger + logger = logger, + subscription = storelessSubscriptionContainer ) val initialMsg = StubChatMessage( @@ -187,7 +193,8 @@ class ChatContainerTest { val container = ChatContainer( repo = repo, channel = channel, - logger = logger + logger = logger, + subscription = storelessSubscriptionContainer ) val initialMsg = StubChatMessage( @@ -258,7 +265,8 @@ class ChatContainerTest { val container = ChatContainer( repo = repo, channel = channel, - logger = logger + logger = logger, + subscription = storelessSubscriptionContainer ) val initialMsg = StubChatMessage( @@ -334,7 +342,8 @@ class ChatContainerTest { val container = ChatContainer( repo = repo, channel = channel, - logger = logger + logger = logger, + subscription = storelessSubscriptionContainer ) val firstMessage = StubChatMessage(order = "B") @@ -398,7 +407,8 @@ class ChatContainerTest { val container = ChatContainer( repo = repo, channel = channel, - logger = logger + logger = logger, + subscription = storelessSubscriptionContainer ) val messages = buildList { diff --git a/domain/src/test/java/com/anytypeio/anytype/domain/object/ObjectWrapperTest.kt b/domain/src/test/java/com/anytypeio/anytype/domain/object/ObjectWrapperTest.kt index 132ab671b5..7ba2f8e4ae 100644 --- a/domain/src/test/java/com/anytypeio/anytype/domain/object/ObjectWrapperTest.kt +++ b/domain/src/test/java/com/anytypeio/anytype/domain/object/ObjectWrapperTest.kt @@ -40,4 +40,97 @@ class ObjectWrapperTest { ).description ) } + + @Test + fun `should parse defaultTemplateId as single value from string`() { + val templateId = MockDataFactory.randomString() + + assertEquals( + expected = templateId, + actual = ObjectWrapper.Type( + map = mapOf( + Relations.DEFAULT_TEMPLATE_ID to templateId + ) + ).defaultTemplateId + ) + } + + @Test + fun `should parse defaultTemplateId as single value from list`() { + val templateId = MockDataFactory.randomString() + + assertEquals( + expected = templateId, + actual = ObjectWrapper.Type( + map = mapOf( + Relations.DEFAULT_TEMPLATE_ID to listOf(templateId) + ) + ).defaultTemplateId + ) + } + + @Test + fun `should parse defaultTemplateId as single value from list with multiple items`() { + val firstTemplateId = MockDataFactory.randomString() + val secondTemplateId = MockDataFactory.randomString() + + assertEquals( + expected = firstTemplateId, + actual = ObjectWrapper.Type( + map = mapOf( + Relations.DEFAULT_TEMPLATE_ID to listOf(firstTemplateId, secondTemplateId) + ) + ).defaultTemplateId + ) + } + + @Test + fun `should return null when defaultTemplateId is null`() { + assertEquals( + expected = null, + actual = ObjectWrapper.Type( + map = mapOf( + Relations.DEFAULT_TEMPLATE_ID to null + ) + ).defaultTemplateId + ) + } + + @Test + fun `should return null when defaultTemplateId is empty list`() { + assertEquals( + expected = null, + actual = ObjectWrapper.Type( + map = mapOf( + Relations.DEFAULT_TEMPLATE_ID to emptyList() + ) + ).defaultTemplateId + ) + } + + @Test + fun `should return null when defaultTemplateId is missing from map`() { + assertEquals( + expected = null, + actual = ObjectWrapper.Type( + map = mapOf( + Relations.NAME to "TestType" + ) + ).defaultTemplateId + ) + } + + @Test + fun `should handle mixed type list and return first valid string for defaultTemplateId`() { + val templateId = MockDataFactory.randomString() + + assertEquals( + expected = templateId, + actual = ObjectWrapper.Type( + map = mapOf( + Relations.DEFAULT_TEMPLATE_ID to listOf(templateId, 123, null) + ) + ).defaultTemplateId + ) + } } \ No newline at end of file diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt index d8fd8d7564..cfc65867de 100644 --- a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt @@ -32,6 +32,8 @@ import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer.Store import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer +import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider +import com.anytypeio.anytype.domain.notifications.NotificationBuilder import com.anytypeio.anytype.domain.objects.CreateObjectFromUrl import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes import com.anytypeio.anytype.domain.objects.getTypeOfObject @@ -83,7 +85,9 @@ class ChatViewModel @Inject constructor( private val exitToVaultDelegate: ExitToVaultDelegate, private val getLinkPreview: GetLinkPreview, private val createObjectFromUrl: CreateObjectFromUrl, - private val notificationPermissionManager: NotificationPermissionManager + private val notificationPermissionManager: NotificationPermissionManager, + private val spacePermissionProvider: UserPermissionProvider, + private val notificationBuilder: NotificationBuilder ) : BaseViewModel(), ExitToVaultDelegate by exitToVaultDelegate { private val visibleRangeUpdates = MutableSharedFlow>( @@ -110,6 +114,20 @@ class ChatViewModel @Inject constructor( // generateDummyChatHistory() + viewModelScope.launch { + spacePermissionProvider + .observe(vmParams.space) + .collect { permission -> + if (permission?.isOwnerOrEditor() == true) { + if (chatBoxMode.value is ChatBoxMode.ReadOnly) { + chatBoxMode.value = ChatBoxMode.Default() + } + } else { + chatBoxMode.value = ChatBoxMode.ReadOnly + } + } + } + viewModelScope.launch { spaceViews .observe( @@ -152,15 +170,20 @@ class ChatViewModel @Inject constructor( } } + fun onResume() { + notificationBuilder.clearNotificationChannel( + spaceId = vmParams.space.id, + chatId = vmParams.ctx + ) + } + private suspend fun proceedWithObservingChatMessages( account: Id, chat: Id ) { combine( - chatContainer - .watchWhileTrackingAttachments(chat = chat).distinctUntilChanged() - , - chatContainer.fetchAttachments(vmParams.space).distinctUntilChanged(), + chatContainer.watchWhileTrackingAttachments(chat = chat).distinctUntilChanged(), + chatContainer.subscribeToAttachments(vmParams.ctx, vmParams.space).distinctUntilChanged(), chatContainer.fetchReplies(chat = chat).distinctUntilChanged() ) { result, dependencies, replies -> Timber.d("DROID-2966 Chat counter state from container: ${result.state}") @@ -663,6 +686,9 @@ class ChatViewModel @Inject constructor( chatBoxAttachments.value = emptyList() chatBoxMode.value = ChatBoxMode.Default() } + is ChatBoxMode.ReadOnly -> { + // Do nothing. + } } } } @@ -895,6 +921,9 @@ class ChatViewModel @Inject constructor( fun onBackButtonPressed(isSpaceRoot: Boolean) { viewModelScope.launch { + withContext(dispatchers.io) { + chatContainer.stop(chat = vmParams.ctx) + } if (isSpaceRoot) { Timber.d("Root space screen. Releasing resources...") proceedWithClearingSpaceBeforeExitingToVault() @@ -1150,6 +1179,9 @@ class ChatViewModel @Inject constructor( abstract val isSendingMessageBlocked: Boolean + data object ReadOnly : ChatBoxMode() { + override val isSendingMessageBlocked: Boolean = true + } data class Default( override val isSendingMessageBlocked: Boolean = false ) : ChatBoxMode() @@ -1170,6 +1202,7 @@ class ChatViewModel @Inject constructor( is ChatBoxMode.Default -> copy(isSendingMessageBlocked = isBlocked) is ChatBoxMode.EditMessage -> copy(isSendingMessageBlocked = isBlocked) is ChatBoxMode.Reply -> copy(isSendingMessageBlocked = isBlocked) + is ChatBoxMode.ReadOnly -> this } } diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModelFactory.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModelFactory.kt index 72ad71dc2c..bfca3d0f3b 100644 --- a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModelFactory.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModelFactory.kt @@ -14,8 +14,8 @@ import com.anytypeio.anytype.domain.misc.GetLinkPreview import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer -import com.anytypeio.anytype.domain.`object`.OpenObject -import com.anytypeio.anytype.domain.`object`.SetObjectDetails +import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider +import com.anytypeio.anytype.domain.notifications.NotificationBuilder import com.anytypeio.anytype.domain.objects.CreateObjectFromUrl import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes import com.anytypeio.anytype.presentation.notifications.NotificationPermissionManager @@ -41,19 +41,21 @@ class ChatViewModelFactory @Inject constructor( private val exitToVaultDelegate: ExitToVaultDelegate, private val getLinkPreview: GetLinkPreview, private val createObjectFromUrl: CreateObjectFromUrl, - private val notificationPermissionManager: NotificationPermissionManager + private val notificationPermissionManager: NotificationPermissionManager, + private val spacePermissionProvider: UserPermissionProvider, + private val notificationBuilder: NotificationBuilder ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T = ChatViewModel( vmParams = params, chatContainer = chatContainer, addChatMessage = addChatMessage, + editChatMessage = editChatMessage, + deleteChatMessage = deleteChatMessage, toggleChatMessageReaction = toggleChatMessageReaction, members = members, getAccount = getAccount, - deleteChatMessage = deleteChatMessage, urlBuilder = urlBuilder, - editChatMessage = editChatMessage, spaceViews = spaceViews, dispatchers = dispatchers, uploadFile = uploadFile, @@ -62,6 +64,8 @@ class ChatViewModelFactory @Inject constructor( exitToVaultDelegate = exitToVaultDelegate, getLinkPreview = getLinkPreview, createObjectFromUrl = createObjectFromUrl, - notificationPermissionManager = notificationPermissionManager + notificationPermissionManager = notificationPermissionManager, + spacePermissionProvider = spacePermissionProvider, + notificationBuilder = notificationBuilder ) as T } \ No newline at end of file diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBox.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBox.kt index 4d12c82f83..a49374e98a 100644 --- a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBox.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBox.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.colorResource @@ -63,6 +64,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import com.anytypeio.anytype.core_models.Url +import com.anytypeio.anytype.core_models.primitives.Space import com.anytypeio.anytype.core_ui.common.DEFAULT_DISABLED_ALPHA import com.anytypeio.anytype.core_ui.common.DefaultPreviews import com.anytypeio.anytype.core_ui.common.FULL_ALPHA @@ -149,10 +151,13 @@ fun ChatBox( ) when(mode) { is ChatBoxMode.Default -> { - + // Do nothing } is ChatBoxMode.EditMessage -> { - + // Do nothing + } + is ChatBoxMode.ReadOnly -> { + // Do nothing } is ChatBoxMode.Reply -> { Box( @@ -366,7 +371,11 @@ fun ChatBox( .clickable { onMessageSent(text.text, spans) clearText() - resetScroll() + // Bypass resetScroll in edit mode because editing a message does not require + // resetting the scroll position, unlike sending a new message. + if (mode !is ChatBoxMode.EditMessage) { + resetScroll() + } showMarkup = false } } @@ -884,6 +893,41 @@ fun ChatBoxEditPanel( } } +@Composable +fun ReaderChatBox(modifier: Modifier = Modifier) { + Row( + modifier = modifier + .fillMaxWidth() + .background( + color = colorResource(R.color.navigation_panel), + shape = RoundedCornerShape(16.dp) + ) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(R.drawable.ic_chatbox_lock), + contentDescription = "Lock icon" + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + modifier = Modifier.padding(start = 12.dp), + text = "Only editors can send messages. Contact the owner to request access.", + style = Caption1Regular, + color = colorResource(R.color.text_primary) + ) + } +} + +@DefaultPreviews +@Composable +fun ReaderChatBoxPreview() { + ReaderChatBox() +} + + sealed class ChatMarkupEvent { data object Bold : ChatMarkupEvent() data object Italic : ChatMarkupEvent() diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBubble.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBubble.kt index 60107d5de3..6c50eda993 100644 --- a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBubble.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBubble.kt @@ -94,7 +94,8 @@ fun Bubble( onScrollToReplyClicked: (ChatView.Message.Reply) -> Unit, onAddReactionClicked: () -> Unit, onViewChatReaction: (String) -> Unit, - onMentionClicked: (Id) -> Unit + onMentionClicked: (Id) -> Unit, + isReadOnly: Boolean = false ) { var showDropdownMenu by remember { mutableStateOf(false) } var showDeleteMessageWarning by remember { mutableStateOf(false) } @@ -302,7 +303,7 @@ fun Bubble( showDropdownMenu = false } ) { - if (!isMaxReactionCountReached) { + if (!isMaxReactionCountReached && !isReadOnly) { DropdownMenuItem( text = { Text( @@ -318,19 +319,21 @@ fun Bubble( ) Divider(paddingStart = 0.dp, paddingEnd = 0.dp) } - DropdownMenuItem( - text = { - Text( - text = stringResource(R.string.chats_reply), - color = colorResource(id = R.color.text_primary), - modifier = Modifier.padding(end = 64.dp) - ) - }, - onClick = { - onReply() - showDropdownMenu = false - } - ) + if (!isReadOnly) { + DropdownMenuItem( + text = { + Text( + text = stringResource(R.string.chats_reply), + color = colorResource(id = R.color.text_primary), + modifier = Modifier.padding(end = 64.dp) + ) + }, + onClick = { + onReply() + showDropdownMenu = false + } + ) + } if (content.msg.isNotEmpty()) { Divider(paddingStart = 0.dp, paddingEnd = 0.dp) DropdownMenuItem( diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatScreen.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatScreen.kt index 4f988bfcd7..1e3f16a892 100644 --- a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatScreen.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatScreen.kt @@ -228,7 +228,10 @@ fun ChatScreenWrapper( onScrollToBottomClicked = vm::onScrollToBottomClicked, onVisibleRangeChanged = vm::onVisibleRangeChanged, onUrlInserted = vm::onUrlPasted, - onGoToMentionClicked = vm::onGoToMentionClicked + onGoToMentionClicked = vm::onGoToMentionClicked, + isReadOnly = vm.chatBoxMode + .collectAsStateWithLifecycle() + .value is ChatBoxMode.ReadOnly ) LaunchedEffect(Unit) { vm.uXCommands.collect { command -> @@ -342,7 +345,8 @@ fun ChatScreen( onScrollToBottomClicked: (Id?) -> Unit, onVisibleRangeChanged: (Id, Id) -> Unit, onUrlInserted: (Url) -> Unit, - onGoToMentionClicked: () -> Unit + onGoToMentionClicked: () -> Unit, + isReadOnly: Boolean = false ) { Timber.d("DROID-2966 Render called with state, number of messages: ${messages.size}") @@ -518,7 +522,8 @@ fun ChatScreen( onViewChatReaction = onViewChatReaction, onMemberIconClicked = onMemberIconClicked, onMentionClicked = onMentionClicked, - onScrollToReplyClicked = onScrollToReplyClicked + onScrollToReplyClicked = onScrollToReplyClicked, + isReadOnly = isReadOnly ) GoToMentionButton( @@ -694,52 +699,61 @@ fun ChatScreen( } } } - ChatBox( - mode = chatBoxMode, - modifier = Modifier - .imePadding() - .navigationBarsPadding(), - chatBoxFocusRequester = chatBoxFocusRequester, - onMessageSent = { text, markup -> - onMessageSent(text, markup) - }, - resetScroll = { - if (!isPerformingScrollIntent.value) { - scope.launch { - lazyListState.scrollToItem(0) - awaitFrame() - while (!isAtBottom) { - val offset = lazyListState.firstVisibleItemScrollOffset - val delta = (-offset).coerceAtLeast(-80) - lazyListState.animateScrollBy(delta.toFloat()) + + if (isReadOnly) { + ReaderChatBox( + modifier = Modifier + .padding(start = 20.dp, end = 20.dp, bottom = 12.dp) + .navigationBarsPadding() + ) + } else { + ChatBox( + mode = chatBoxMode, + modifier = Modifier + .imePadding() + .navigationBarsPadding(), + chatBoxFocusRequester = chatBoxFocusRequester, + onMessageSent = { text, markup -> + onMessageSent(text, markup) + }, + resetScroll = { + if (!isPerformingScrollIntent.value) { + scope.launch { + lazyListState.scrollToItem(0) awaitFrame() + while (!isAtBottom) { + val offset = lazyListState.firstVisibleItemScrollOffset + val delta = (-offset).coerceAtLeast(-80) + lazyListState.animateScrollBy(delta.toFloat()) + awaitFrame() + } } } - } - }, - attachments = attachments, - clearText = { - text = TextFieldValue() - }, - onAttachObjectClicked = onAttachObjectClicked, - onClearAttachmentClicked = onClearAttachmentClicked, - onClearReplyClicked = onClearReplyClicked, - onChatBoxMediaPicked = onChatBoxMediaPicked, - onChatBoxFilePicked = onChatBoxFilePicked, - onExitEditMessageMode = { - onExitEditMessageMode().also { + }, + attachments = attachments, + clearText = { text = TextFieldValue() - } - }, - onValueChange = { t, s -> - text = t - spans = s - onTextChanged(t) - }, - text = text, - spans = spans, - onUrlInserted = onUrlInserted - ) + }, + onAttachObjectClicked = onAttachObjectClicked, + onClearAttachmentClicked = onClearAttachmentClicked, + onClearReplyClicked = onClearReplyClicked, + onChatBoxMediaPicked = onChatBoxMediaPicked, + onChatBoxFilePicked = onChatBoxFilePicked, + onExitEditMessageMode = { + onExitEditMessageMode().also { + text = TextFieldValue() + } + }, + onValueChange = { t, s -> + text = t + spans = s + onTextChanged(t) + }, + text = text, + spans = spans, + onUrlInserted = onUrlInserted + ) + } } } @@ -760,6 +774,7 @@ fun Messages( onMemberIconClicked: (Id?) -> Unit, onMentionClicked: (Id) -> Unit, onScrollToReplyClicked: (Id) -> Unit, + isReadOnly: Boolean = false ) { // Timber.d("DROID-2966 Messages composition: ${messages.map { if (it is ChatView.Message) it.content.msg else it }}") val scope = rememberCoroutineScope() @@ -848,7 +863,8 @@ fun Messages( onViewChatReaction = { emoji -> onViewChatReaction(msg.id, emoji) }, - onMentionClicked = onMentionClicked + onMentionClicked = onMentionClicked, + isReadOnly = isReadOnly ) } if (idx == messages.lastIndex) { diff --git a/feature-chats/src/main/res/drawable/ic_chatbox_lock.xml b/feature-chats/src/main/res/drawable/ic_chatbox_lock.xml new file mode 100644 index 0000000000..f0b04268bc --- /dev/null +++ b/feature-chats/src/main/res/drawable/ic_chatbox_lock.xml @@ -0,0 +1,10 @@ + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 387eb40b6d..e15169b90d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -middlewareVersion = "v0.41.0-rc13" +middlewareVersion = "v0.41.0-rc16" kotlinVersion = '2.0.21' kspVersion = "2.0.21-1.0.25" diff --git a/localization/src/main/res/values-be-rBY/strings.xml b/localization/src/main/res/values-be-rBY/strings.xml index 5f54c2c948..550c8a4d3d 100644 --- a/localization/src/main/res/values-be-rBY/strings.xml +++ b/localization/src/main/res/values-be-rBY/strings.xml @@ -1242,6 +1242,7 @@ %1$s далучыўся(-лась) да прасторы %2$s з правам доступу да рэдагавання Запыт ухвалены Ваш запыт на далучэнне да прасторы %1$s быў ухвалены з правамі доступу толькі для чытання. Прастора будзе даступна на вашай прыладзе ў бліжэйшы час. + Your request to join the %1$s space has been approved. The space will be available on your device soon. Ваш запыт на далучэнне да прасторы %1$s быў ухвалены з правамі доступу да рэдагавання. Прастора будзе даступна на вашай прыладзе ў бліжэйшы час. The space \"%1$s\" is no longer accessible. Вы былі выдалены з прасторы \"%1$s\", або прастора была выдалена ўладальнікам. diff --git a/localization/src/main/res/values-de-rDE/strings.xml b/localization/src/main/res/values-de-rDE/strings.xml index 312061e828..c9a4fdd88c 100644 --- a/localization/src/main/res/values-de-rDE/strings.xml +++ b/localization/src/main/res/values-de-rDE/strings.xml @@ -1234,6 +1234,7 @@ %1$s ist dem Space %2$s mit Bearbeitungsrechten beigetreten Anfrage genehmigt Deine Anfrage, dem Space %1$s beizutreten, wurde mit Nur-Lese-Rechten genehmigt. Der Bereich wird in Kürze auf deinem Gerät verfügbar sein. + Your request to join the %1$s space has been approved. The space will be available on your device soon. Deine Anfrage, dem Space %1$s beizutreten, wurde mit Bearbeitungszugriffsrechten genehmigt. Der Space wird in Kürze auf deinem Gerät verfügbar sein. The space \"%1$s\" is no longer accessible. You have been removed from the space \"%1$s\", or the space was deleted by the owner. diff --git a/localization/src/main/res/values-es-rES/strings.xml b/localization/src/main/res/values-es-rES/strings.xml index 9ffb56588c..accca9c451 100644 --- a/localization/src/main/res/values-es-rES/strings.xml +++ b/localization/src/main/res/values-es-rES/strings.xml @@ -1164,13 +1164,13 @@ Acceder a un espacio Comentario privado para el propietario del espacio Cuando el propietario del espacio apruebe tu solicitud, tendrás acceso al espacio con los derechos que el propietario determine. - Join %1$s - You\'ve been invited to join %1$s, created by %2$s + Acceder a %1$s + Te han invitado a unirte a %1$s, un espacio creado por %2$s Acceder al espacio Solicitud enviada Recibirás una notificación cuando el propietario del espacio apruebe tu solicitud. - Manage spaces - Hang tight — we’re setting things up for you. This should only take a moment. + Gestionar espacios + Espera, estamos configurándolo todo. Será solo un momento. Solicitar acceso Tienes una invitación al espacio %1$s, creado por %2$s. Envía una solicitud para que el propietario del espacio te dé acceso. Escribe tu comentario @@ -1234,6 +1234,7 @@ %1$s se ha incorporado al espacio %2$s con derechos de acceso de edición Solicitud aprobada Tu solicitud para acceder al espacio %1$s se ha aprobado con derechos de solo lectura. El espacio estará pronto disponible en tu dispositivo. + Tu solicitud de acceso al espacio %1$s se ha aprobado. El espacio estará pronto disponible en tu dispositivo. Tu solicitud de acceso al espacio %1$s se ha aprobado con derechos de edición. El espacio estará pronto disponible en tu dispositivo. El espacio «%1$s» ya no está accesible. Te han eliminado del espacio «%1$s» o el propietario ha eliminado ese espacio. @@ -1586,7 +1587,7 @@ En concreto, Botón Crear objeto Chat Copiar - Copy Text + Copiar texto Editar mensaje editado Aún no hay ningún mensaje.\nPuedes escribir tú el primero. diff --git a/localization/src/main/res/values-fr-rFR/strings.xml b/localization/src/main/res/values-fr-rFR/strings.xml index 7cc79f729d..e6678446f7 100644 --- a/localization/src/main/res/values-fr-rFR/strings.xml +++ b/localization/src/main/res/values-fr-rFR/strings.xml @@ -709,7 +709,7 @@ Dans l’application sur votre ordinateur, allez dans Paramètres – Clé pour trouver le code QR permettant de vous connecter. Ok Choose option - Find a property + Trouver une propriété Inbox Type inconnu Collection @@ -794,7 +794,7 @@ déplacé vers Votre Clé a été copiée Paramètres de type - Property settings + Paramètres de la propriété Supprimer Cet objet n\'a pas de liens vers d\'autres objets.\nEssayez d\'en créer un nouveau. Cet objet n\'a pas de liens vers d\'autres objets. @@ -972,7 +972,7 @@ Types are like categories that help you group and manage your objects. All objects are connected. Use properties to build connections between objects. Create a type - Create a property + Créer une propriété Mes types Mes propriétés Bibliothèque @@ -1009,7 +1009,7 @@ Ceci est votre clé It replaces login and password. Keep it safe — you control your data. You can find this Key later in app settings. Afficher ma clé - Reveal and copy + Révéler et copier Ignorer Plus tard Copier dans le presse-papier @@ -1164,9 +1164,9 @@ Rejoindre un espace Commentaire privé pour un propriétaire d\'espace Une fois que le propriétaire de l\'espace a approuvé votre demande, vous rejoindrez l\'espace avec les droits d\'accès déterminés par le propriétaire. - Join %1$s + Rejoindre %1$s You\'ve been invited to join %1$s, created by %2$s - Join Space + Rejoindre l\'espace Demande envoyée Vous recevrez une notification lorsque le propriétaire de l\'espace approuvera votre demande. Manage spaces @@ -1234,6 +1234,7 @@ %1$s a rejoint l\'espace %2$s avec droits d\'accès en modification Demande approuvée Votre demande de rejoindre l\'espace %1$s a été approuvée avec des droits d\'accès en lecture seule. L\'espace sera bientôt disponible sur votre appareil. + Votre demande de rejoindre l\'espace %1$s a été approuvée. L\'espace sera bientôt disponible sur votre appareil. Votre demande de rejoindre l\'espace %1$s a été approuvée avec les droits d\'accès en modification. L\'espace sera bientôt disponible sur votre appareil. L\'espace \"%1$s\" n\'est plus accessible. Vous avez été retiré de l\'espace \"%1$s\", ou l\'espace a été supprimé par le propriétaire. @@ -1583,7 +1584,7 @@ Merci de décrire ici vos besoins spécifiques. Créer un bouton objet Messagerie Copier - Copy Text + Copier le texte Modifier le message modifié Il n\'y a pas encore de messages.\nSoyez le premier à lancer une discussion. @@ -1602,7 +1603,7 @@ Merci de décrire ici vos besoins spécifiques. Créez vos premiers objets pour commencer. Supprimer complètement Open Query - Create Query + Créer une requête Définir par défaut Éditer Dupliquer @@ -1630,7 +1631,7 @@ Merci de décrire ici vos besoins spécifiques. Ce type n\'a pas\nde modèle Local Masqué - Local properties + Propriétés locales Ces propriétés existent uniquement dans cet objet et ne font pas partie de son type. Ajouter au type à utiliser dans tous les objets, ou supprimer. No properties yet. Add some to this type Failed to open the object type @@ -1643,7 +1644,7 @@ Merci de décrire ici vos besoins spécifiques. Samedi Dimanche Fichier - Attachment + Pièce jointe Créez votre premier espace pour commencer C\'est vide ici. Émoticônes & Personnes @@ -1758,7 +1759,7 @@ Merci de décrire ici vos besoins spécifiques. Mes Types System Types Propriétés - My Properties + Mes propriétés System Properties Déplacer vers la corbeille Erreur lors de la création du compte : l\'espace est manquant @@ -1769,7 +1770,7 @@ Merci de décrire ici vos besoins spécifiques. Entrez votre adresse e-mail Continuer Ignorer - Incorrect email + E-mail incorrect Enter name Activer les notifications push Recevez une notification instantanée lorsque quelqu\'un vous envoie des messages ou vous mentionne dans vos espaces. diff --git a/localization/src/main/res/values-in-rID/strings.xml b/localization/src/main/res/values-in-rID/strings.xml index 94f20468df..444c93c58d 100644 --- a/localization/src/main/res/values-in-rID/strings.xml +++ b/localization/src/main/res/values-in-rID/strings.xml @@ -1230,6 +1230,7 @@ %1$s bergabung ke ruang %2$s dengan akses penyunting Permintaan disetujui Permintaanmu bergabug ke ruang %1$s telah disetujui dengan hak akses pembaca. Ruang ini akan tersedia di perangkatmu sebentar lagi. + Your request to join the %1$s space has been approved. The space will be available on your device soon. Permintaanmu bergabung ke ruang %1$s telah disetujui dengan hak akses penyunting. Ruang ini akan tersedia di perangkatmu sebentar lagi. Ruang \"%1$s\" tidak dapat diakses lagi. Kamu telah dikeluarkan dari ruang \"%1$s\", atau ruang tersebut telah dihapus oleh pemiliknya. @@ -1573,7 +1574,7 @@ Harap berikan detail spesifik kebutuhan Anda di sini. Tombol buat objek Obrolan Salin - Copy Text + Salin Teks Suting pesan disunting Belum ada pesan.\nJadilah yang memulai diskusi! diff --git a/localization/src/main/res/values-it-rIT/strings.xml b/localization/src/main/res/values-it-rIT/strings.xml index 6cd5fe372d..f0bffbb27a 100644 --- a/localization/src/main/res/values-it-rIT/strings.xml +++ b/localization/src/main/res/values-it-rIT/strings.xml @@ -1235,6 +1235,7 @@ %1$s si è unito allo spazio %2$s con diritti di accesso di modifica Richiesta approvata La tua richiesta di unirti allo spazio %1$s è stata approvata con diritti di accesso in sola lettura. Lo spazio sarà disponibile sul tuo dispositivo a breve. + Your request to join the %1$s space has been approved. The space will be available on your device soon. La tua richiesta di unirti allo spazio %1$s è stata approvata con diritti di accesso per la modifica. Lo spazio sarà disponibile sul tuo dispositivo a breve. Lo spazio \"%1$s\" non è più accessibile. Sei stato rimosso dallo spazio \"%1$s\", oppure lo spazio è stato eliminato dal proprietario. diff --git a/localization/src/main/res/values-ja-rJP/strings.xml b/localization/src/main/res/values-ja-rJP/strings.xml new file mode 100644 index 0000000000..1877cc9e27 --- /dev/null +++ b/localization/src/main/res/values-ja-rJP/strings.xml @@ -0,0 +1,1770 @@ + + + このアプリについて + アプリバージョン + ライブラリ + ユーザーID + + プロフィール + パーソナライズ + 外観 + ファイルストレージ + + ファイルキャッシュを消去 + デバッグ同期レポート + デバッグ + 保管庫 + 保管庫をリセット + 保管庫を削除 + スペースデバッグ + データ + モード + ログアウト + PINコード + アクセス権 + 壁紙 + 技術情報 + ライト + ダーク + 標準 + 設定 + 鍵をバックアップしましたか? + 保管庫へのログインに必要です。紛失しないようにご自身で大切に保管してください。紛失した場合、保管庫に入ることができなくなります。 + ヘルプ&コミュニティ + 更新情報 + Anytypeコミュニティ + ヘルプ&チュートリアル + 規約 + 利用規約 + プライバシーポリシー + 謝辞 + アプリバージョン: %1$s\nビルド番号: %2$d\nライブラリバージョン: %3$s\nアカウント\u00A0ID:\u00A0%4$s\nデバイス\u00A0ID:\u00A0%5$s\nアナリティクス\u00A0ID:\u00A0%6$s + アプリバージョン: %1$s\nビルド番号: %2$d\nライブラリバージョン: %3$s\nアカウント\u00A0ID:\u00A0%4$s\nデバイス\u00A0ID:\u00A0%5$s\nアナリティクス\u00A0ID:\u00A0%6$s\nイーサリアムアドレス\u00A0ID:\u00A0%7$s + デバイス: %1$s\nAndroidバージョン: %2$d\nアプリバージョン: %3$s\nビルド番号: %4$d\nライブラリバージョン: %5$s\nアカウント\u00A0ID:\u00A0%6$s\nデバイス\u00A0ID:\u00A0%7$s\nアナリティクス\u00A0ID:\u00A0%8$s + デバイス: %1$s\nAndroidバージョン: %2$d\nアプリバージョン: %3$s\nビルド番号: %4$d\nライブラリバージョン: %5$s\nアカウント\u00A0ID:\u00A0%6$s\nデバイス\u00A0ID:\u00A0%7$s\nアナリティクス\u00A0ID:\u00A0%8$s\nイーサリアムアドレス\u00A0ID:\u00A0%9$s + スペース + スペース名 + マイスペース + ランダムな単色を適用 + 画像をアップロード + 画像をアップロード + 画像を削除 + スペースを削除 + %1$sまで無料で暗号化されたバックアップノードに保存することができます。上限に達すると、デバイス内だけで保存されるようになります。 + お使いのデバイスの空き容量を増やしたい場合は、すべてのファイルを暗号化されたバックアップノードに移行することができます。ファイルは開く際に自動的に読み込まれます。 + ファイル管理 + ローカルストレージ + ファイルを移行 + %1$s 使用中 + %1$s / %2$s 使用中 + リモートストレージ + お問い合わせ + さらに多くのスペースを入手 + 名前 + スペース設定 + データ管理 + 削除申請は30日以内であれば取り消すことが可能です。30日経過すると、あなたの暗号化された保管庫のデータはバックアップノードから完全に削除され、新しいデバイスからAnytypeにログインすることは出来なくなります。 + 危険な操作 + タイプに追加されました + メンバーシップ + 変換 + 移動 + 変換 + ブロックを追加 + テキスト + 見出し1 + 見出し2 + 見出し3 + 引用文 + チェックボックス + 箇条書きリスト + 番号付きリスト + トグル + タスク + ページ + オブジェクトにリンク + データベース + クエリ + 連絡先 + 既存のツール + ファイル + 画像 + 動画 + ブックマーク + 区切り線 + 区切り点 + コードスニペット + テキスト + リスト + ページ + その他 + ツール + メディア + オブジェクト + プロパティ + 削除 + 複製 + メンション + 共有 + アクション + Aa + 文字色 + 背景色 + ブックマークアイコン + ファイルアイコン + モーダルアイコン + タップして入力 + ギャラリー + ボード + テーブル + リスト + キャンセル + あとで + 追加 + 複製 + ビューを編集 + ボードビュー + プロパティ + 並び替え + 並び替えが設定されていません。 + フィルターが設定されていません。 + オプションが設定されていません。 + フィルター + フィルター + ビュー + プロパティ + 見出し1 + テキスト + セレクト + 人物 + メールアドレス + 数字 + 複数セレクト + 日付 + ファイルとメディア + チェックボックス + URL + 電話番号 + 新しく追加 + 変換 + 並べ替え + 完了 + プロパティ名 + プロパティの種類 + 非表示 + 表示 + ビュー名 + 詳細 + 詳細の名前 + 詳細の種類 + 見出し1 + 名前 + 見出し1 + 引用文 + 見出し2 + 見出し3 + チェックボックス + 動画をアップロード + 画像をアップロード + ファイルをアップロード + 音声をアップロード + ブックマークを追加 + 箇条書きリスト + 番号付きリスト + トグルブロック + リンクを貼り付け + 何もありません。タップして作成 + 無題 + 無題のクエリ + 無題のコレクション + アーカイブ済み + 読み込み中… + 読み込み中です。少々お待ち下さい... + ファイルを読み込み中です。少々お待ち下さい... + 読み込みエラー + 画像読み込みエラー + 画像付きブロック + ブックマークURLを挿入 + アーカイブ + ブロックアイコンを追加 + ブックマークエラーメニューボタン + 問題が発生しました。もう一度お試しください。 + 削除されたタイプ + タイプ: 削除されました + 太字 + イタリック + 取り消し線 + コード + リンク + 文字色 + 背景 + コピー + 切り取り + 貼り付け + スタイル + テキスト + 文字色 + 背景 + 閉じるボタン + 下にブロックを追加 + 変換 + ページに変換 + 削除 + ゴミ箱へ + 複製 + 移動 + スタイル + 文字色 + 背景 + ダウンロード + 置き換え + キャプションを追加 + 名前を変更 + スクロール & 移動 + その他 + 完了 + まずはブロックを選択してください + Plain Text + 元に戻す + やり直し + コピー + 貼り付け + セレクト + 検索 + 検索... + プロパティ + 項目 + ダウンロード + ビュー + ビュー + 絵文字を選択 + logo_transition + リンク元 + リンク先 + 検索 + フィルター + 移動 + 移動 + プレビュー + リンク + テンプレート作成 + リンク + リンクを削除 + オブジェクトを作成 + 日付 + オブジェクト + オブジェクトを作成 + 検索 + 同期中… + このクエリのタイプを選択 + 新しいクエリ + 新しいタイプを作成 + + %dページを選択中 + + 新規 + すべて + 準備中... + 接続なし + 同期中… + 同期完了 + 未同期 + 同期を初期化中 + Anytypeノードに接続されていません + ノードにデータを転送中 + Anytype Networkへのバックアップ + Anytypeステージ環境へのバックアップ + ローカルのみ + セルフホストネットワークでのバックアップ + 同期に失敗しました。再試行中... + バージョンが古くなっています。Anytype を更新してください + 説明 + 見出し1 + 数字 + 日付 + セレクト + ファイルとメディア + チェックボックス + URL + メールアドレス + 電話番号 + 絵文字 + オブジェクトのつながり + プロパティ + 条件を満たすとき + 値を入力 + プロパティ + 並び替えを追加 + 適用 + フィルターを追加 + 複数セレクト + 不明なオブジェクト数 + 昇順 + 降順 + 最初 → 最後 + 最後 → 最初 + 1 → 9 + 9 → 1 + チェック済み → 未チェック + 未チェック → チェック済み + A → Z + Z → A + \"%1$s\" + すべて選択 + + 未入力 + テキストを追加… + 数字を追加... + 不明なタイプ + 短いテキスト + テキスト + プロパティを作成 + 削除されたプロパティ + カバー画像 + カバー画像の表示 + 単色 + グラデーション + 消去 + オプション「%1$s」を作成 + プロパティ + その他のプロパティ + タイプ「%1$s」から + 説明を追加 + チェック済み + 未チェック + URLを貼り付け、または入力 + コールアウト + 引用文 + 見出し1 + 見出し2 + 見出し3 + 新しく作成 + プロパティ「%1$s」を作成 + 名前は必須です + お気に入り + テンプレート + テンプレート + レイアウトの種類 + ロック + ロック解除 + 復元 + お気に入り解除 + ビューを削除 + タグを選択 + オプションを選択 + オブジェクトを選択 + 日付を選択 + 値なし + ソース + クエリ + 条件を指定 + オブジェクト + 作成したオブジェクト + Webページ + ファイルを選択 + U + テキストを入力 + 数字を入力 + 日付を入力 + URLを入力 + メールアドレスを入力 + 電話番号を入力 + スクロールして新しい位置を選択 + 下に追加 + 戻るボタン + 共有ボタン + ホームボタン + 検索ボタン + ドキュメントを追加 + 未対応ブロック + 新しいプロパティ + リンクをコピー + リンクを編集 + リンク解除 + レイアウトの種類を選択 + ユーザー設定 + デフォルトのタイプ + 基本 + プロフィール + 参加者 + タスク + クエリ + コレクション + ノート + ノート + 🗒 + あとで見返すメモや覚え書き + ページ + 📄 + 一般的なページ + 存在しないオブジェクト + データがありません + ソースがありません + 削除されたオブジェクト + 削除済み + リンクを貼り付け、または検索 + クリップボードから貼り付け + オブジェクト「%1$s」を作成 + 基本 + 標準的なレイアウト + ノート + 瞬時に考えを書き留める + プロフィール + 会社、連絡先、友人、家族 + タスク + チェックボックス付きでタスク管理 + ゴミ箱へ + 元に戻す/やり直し + オブジェクト削除中 + 少々お待ちください。まもなく削除されます。 + + @string/none + オブジェクトの説明 + コンテンツプレビュー + なし + + + 表示 + カード + インライン + ビューを変更 + クエリ (インライン) + コレクション (インライン) + 画像を合わせる + 画像プレビュー + アイコンを非表示 + 説明 + タイプ + レイアウトをプレビュー + 不明 + 不明なエラー + ブックマーク + テキスト + リンク + 次へ + タイプを制限 + ご利用いただけなくなるのは誠に残念ではございますが、削除申請は30日以内であれば取り消すことが可能です。30日経過後、保管庫のデータはバックアップノードから完全に消去されます。 + 下にスワイプしてスキップ + 表示 + このタイプの%1$d種類のテンプレート + 種類 + 復元 + 消去 + 文字色 + スタイル + スタイルをリセット + 左に挿入 + 右に挿入 + 左へ移動 + 右へ移動 + 上に挿入 + 下に挿入 + コピー + 並び替え + 上へ移動 + 下へ移動 + セル + + + オブジェクトを開く + ソースを開く + オブジェクト内容を再読み込み + リンクを開く + ファイルを開く + メールアドレスをコピー + 電話番号をコピー + メールを送信 + 電話番号に発信 + ブックマークアイコン + 1セルを選択中 + %1$dセルを選択中 + 1列を選択中 + %1$d列を選択中 + 1行を選択中 + %1$d行を選択中 + 1個のブロックを選択中 + %1$d個のブロックを選択中 + 選択されたブロックが見つかりませんでした + ブロック選択エラー + 未定義 + プロパティ: %1$s + プロパティ: %1$s + タイプ: %1$s + 作成したタイプ + 作成したプロパティ + タイプ「%1$s」が追加されました + データソースとなるクエリ名 + クエリ (インライン)にソースがありません + クエリ (インライン)にタイプがありません + 最近開かれた + Anytypeライブラリ + デフォルトリスト + 標準 + お気に入りのオブジェクト + このデバイス上 + 作成したオブジェクト一覧 + ビュー変更ボタン + オブジェクトがありません + まずはオブジェクトを作成してください + オブジェクトを作成 + 条件が指定されていません + 条件を指定して、同じタイプやプロパティを持つオブジェクトをまとめましょう。変更は自動的に反映されます。 + 条件を指定 + オブジェクトがありません + まずはオブジェクトを作成してください + オブジェクトを作成 + クエリまたは、コレクションにビューがありません。オブジェクトを再作成してください。 + クエリをコレクションに変換 + クエリを変更 + コレクション + 作成したコレクション一覧 + タイプ「%1$s」が見つかりませんでした + 新しく作成、もしくは検索 + 別の条件を試してください。 + 壁紙を変更 + アイコンを変更 + おまかせ + 選択 + 属性 + これらのファイルを完全に削除しますか? + この操作を取り消すことはできません。 + キャンセル + 削除 + タイプを変更 + タイプを開く + クエリ %1$sを開く + クエリ %1$sを作成 + このビューは削除できません + オブジェクトを作成 + レイアウト + カバー画像 + なし + このビューのデフォルト + テンプレートを編集 + 複製 + 削除 + 未対応 + デフォルトにする + デフォルトのオブジェクト + デフォルトテンプレート + デフォルト + このタイプはテンプレートに対応していません + %1$d個が適用済み + リスト + テーブル + ギャラリー + ボード + カバー画像 + ボタン + しばらくお待ちください。まもなくです... + 確定 + 招待コードがnullです + 招待コード + お持ちでない場合は、anytype.ioに行き、待機リストにサインアップしてください。順次、招待を始めています。 + Anytypeではメモ書きから、書類作成、タスク管理、ファイル共有、ウェブコンテンツの保存まであらゆる情報を管理することができます。 + ログイン + 新規登録 + 鍵を入力 + 鍵でログイン + QRコードをスキャン + PINコード入力 + おめでとうございます! + 更新が必要です + 新しいAnytypeで作成または変更されたデータがあります。\nすべてのデータと最新機能を利用するには、アプリを更新する必要があります。 + 更新 + あとで + さあ、始めましょう! + 初めてのプロフィールが完成しました! + デバイス内に保存され、共有するまで他者に公開されることはありません! + プロフィールを選択 + ログアウト + プロフィールを追加 + 名前や\nプロフィール画像を追加 + 名前を入力 + 作成 + T + あとで行う + PINコードを入力 + PINコードの確認 + プロフィールの切り替え + プロフィール画像 + 矢印アイコン + ユーザー設定 + その他の設定 + 公開ページ + 更新情報 + 保管庫へのログインに必要です。紛失しないようにご自身で大切に保管してください。紛失した場合、保管庫に入ることができなくなります。 + 鍵をバックアップ + 書き留めました + witch collapse practice feed shame open despair creek road again ice least lake tree young address brain despair + 鍵を表示してコピー + %1$sさん、こんにちは! + こんにちは! + 招待コードを入力 + Anytypeへようこそ + ウォレットを設定中... + + 戻る + 新しいプロフィールを作成 + ピアがありません + 戻るボタンのアイコン + URLを貼り付け、または入力 + リンク解除 + リンク + メディアブロックでファイルを読み込むには読み取り権限が必要です。 + ユーザーによって権限が拒否されました。デバイスの設定から、アプリの設定を開き、「権限」もしくは「許可」で必要な権限を許可してください。 + 読み取り権限が拒否され、再度許可を求められません。デバイスの設定から、アプリの設定を開き、「権限」もしくは「許可」で必要な権限を許可してください。 + デバイスにファイルを読み込むには書き込み権限が必要です。 + 書き込み権限が拒否されました。デバイスにファイルを読み込むには許可してください。 + 書き込み権限が拒否され、再度許可を求められません。 + 許可 + 拒否 + はい + エラー + ページアイコン + 削除 + 作成 + URLを貼り付け、または入力 + メニューアイコン + 再試行後、ウォレットの起動に失敗しました + ドライブファイルが選択されました + ファイルが不明なプロバイダから選択されました + ローカルファイルが選択されました + ファイルが選択されるのを待っています... + 壁紙 + 壁紙アイコン + キーチェーンアイコン + ピン留めアイコン + ページとして開く + 開く + オブジェクト「%1$s」が見つかりませんでした + 新しく作成、もしくは検索 + 検索するページがありません。 + このページにリンク + リンク + このページに移動 + 移動 + 保管庫を取得中… + ページ内検索 + ビューの種類 + タイプ名 + 名前 + プロパティ名 + 新規 + プラスボタン + 画像アイコン + 絵文字アイコン + 並び替え + タップして入力 + ギャラリー + ビュー + ボード + テーブル + リスト + 入力 + キャンセル + テキストを入力 + 数字を入力してください + ビュー名 + 削除 + プロパティを表示 + 作成 + 新しいビュー + まもなく登場 + プロパティ + ビューを編集 + 開いて編集 + このクエリから削除 + リセット + プロパティを検索 + チェックアイコン + 未入力の値を下に並び替え + 未入力の値を上に並び替え + 一致 + 今日 + 昨日 + 明日 + 選択した日付を開く + 日付なし + 特定の日付 + 並べ替え + 画像を選択 + 幅の推奨サイズは1080ピクセル + カバー画像のサイズ + 画像をアップロード + ギャラリー + タイプを変更 + カバー画像を変更 + 削除 + オプションを選択 + タグを検索 + フィルターするプロパティを選択 + 並び替えるプロパティを選択 + リストに追加 + 追加 + ギャラリーからアップロード + 外部ストレージからアップロード + アップロード + ログイン用QRコードは、デスクトップアプリの設定から「鍵」を開くと、確認できます。 + はい + オプションを選択 + プロパティを検索 + 受信トレイ + 不明なタイプ + コレクション + レイアウト + プロパティ + レイアウト + テンプレート + アーカイブから復元 + アイコン + オブジェクトの絵文字または画像 + カバー画像 + 背景画像 + オブジェクトの配置 + 関連するオブジェクト一覧 + オブジェクトの更新履歴 + 更新履歴 + オブジェクトデバッグ + 診断情報 + 新しいオブジェクト名 + まもなく + 本当に1個のオブジェクトを削除してもよろしいですか? + 本当に%1$d個のオブジェクトを削除してもよろしいですか? + これらのオブジェクトは完全に削除されます。この操作は取り消せません。 + このオブジェクトは完全に削除されます。この操作は取り消せません。 + 共有済み + このオブジェクトは存在しません + ダッシュボードへ戻る + 前に戻る + このタイプのクエリが見つかりません。 + 作成 + 新しい %1$s + 閉じる + 設定に移動 + 必ず、ご自分の鍵を大切に保管してください + 設定画面からいつでも鍵を確認することができます。 + 鍵を紛失してログアウトすると、データに二度とアクセスできなくなります。万が一に備えて、この鍵をAnytype以外の場所に保管してください。 + カードサイズ + キャッシュを消去 + 本当によろしいですか? + このデバイスから、Anytypeに保存されたすべてのメディアファイルが削除されます。削除されたファイルは、バックアップノードや他のデバイスからダウンロードされます。 + 新しいプロフィール + 鍵をバックアップ + Anytype Analytics + Anytypeの改善のため、皆様の利用状況を参考にさせていただいております。このアプリにはプライバシーに配慮した利用状況を分析するコードが含まれております。\n皆様のドキュメント内容が記録されることはありませんので、ご安心ください。\n今後、分析のオプトアウト機能を提供予定ですので、引き続きニュースレターの購読をお願いいたします。 + 再試行 + もう一度実行 + Unsplash + 削除を取り消す + ログアウトしてデータを消去 + この保管庫は%1$d日後に削除されます + この保管庫は明日に削除されます + この保管庫は本日中に削除されます + この保管庫は?日後に削除されます + 本当に保管庫を削除してもよろしいですか? + すべてのデバイスからログアウトされます。30日以内であれば、保管庫を復元できます。その後、完全に削除されます。 + テンプレートを選択 + スワイプして選択 + テンプレートなし + Unsplashで画像を検索中にエラーが発生しました。あとでもう一度お試しください。 + 必ず、ご自分の鍵を大切に保管してください + プロパティを追加するには、ロックを解除してください + プロパティを編集するには、ロックを解除してください + プロパティの種類を選択 + 復元 + 保管庫が削除されました。 + + %dを選択中 + + debug_mode + trouble_mode + 設定 + ネットワークモード + セルフホストの設定 + ファイルを選択 + + Anytype + ローカルモード + セルフホスト + + をリンクしました リンク先 : + を移動しました 移動先 : + 鍵がコピーされました + タイプ設定 + プロパティ設定 + 削除 + 他のオブジェクトとのリンクがありません。\n新しく作成してみましょう。 + 他のオブジェクトとのリンクがありません。 + オブジェクトがありません。\n新しく作成してみましょう。 + まだオブジェクトがありません。 + オブジェクトがありません。\n新しく作成してみましょう。 + オブジェクトがありません。\n新しく作成してみましょう。 + 絵文字 + あともう少しです! + まだ移行していません + まだ移行していません + このアプリを使用するには、保管庫の移行が必要です。以下の手順のどれかに従ってください。 + 移行が完了しました + こちら + 1. デスクトップ版をダウンロード + \n2. ログインし、最新版にアップデートする。\n3. 移行手順に従う。 + フォーラムを開く + 問題が発生した可能性があります。フォーラムにアクセスし、移行手順を読み、チームにお尋ねください。 + 互換性のないバージョン + タグが見つかりませんでした + 新しく作成 + ローカルモード + Anytypeチーム担当者の方へ\nストレージ容量の上限に達したため、容量の追加を希望します。現在の上限は%1$sです。アカウントIDは%2$sです。ご対応のほど、よろしくお願いいたします。%3$s + アプリに移動 + 終了中... 少々お待ち下さい + 読み込み中... 少々お待ち下さい + デフォルト + スペースを作成 + 問題が発生しました。もう一度お試しください。 + 種類 + 種類 + スペース情報 + 作成日 + 作成者 + スペースID + ネットワークID + このスペースは完全に削除されます。この操作を元に戻すことはできません。すべてのデータが完全に削除され、永久にアクセスできなくなりますので、ご注意ください + スペース「%1$s」を削除 + 本当にスペースを削除してもよろしいですか? + スペースIDがコピーされました + ネットワークIDがコピーされました + 作成者のIDがコピーされました + 新しいAnytypeで作成または変更されたデータがあります。すべてのデータと最新機能を利用するには、アプリを更新する必要があります。 + 更新が必要です + デフォルト + マイスペース + 非公開 + 非公開スペース + 共有済み + 共有スペース + 不明なタイプ + はじめる + リスト + オブジェクト + 編集 + すべて選択を解除 + クエリ + お気に入り + 最近編集された + 最近開かれた + ゴミ箱 + 同期されたファイル + ログイン + Anytypeライブラリ + ごみ箱が空です。 + すべて片付いていますね! + + スタイル + メディア + オブジェクト + オブジェクト + プロパティ + その他 + アクション + 配置 + 文字色 + 背景 + テキスト + テキストを書き始めましょう + 見出し1 + 章、段落もしくはセクションの大きな見出し + 見出し2 + テキストを書き始めましょう + 見出し3 + テキストを書き始めましょう + 引用文 + 引用文を挿入 + コールアウト + アイコン付きテキスト + チェックボックス + やることリストを作成し、タスク管理する + 箇条書きリスト + テキストを書き始めましょう + 番号付きリスト + 番号が付いたリスト + トグル + コンテンツの表示と非表示を切り替えられます + 太字 + イタリック + 取り消し線 + コード + リンク + 下線 + ファイル + ファイルをそのまま保存 + 画像 + 画像 + 画像でページに彩を添えましょう + 動画 + 再生可能な動画をアップロード + ブックマーク + よく使うお気に入りのサイトを保存 + コードスニペット + コードスニペットを挿入します + 区切り線 + 区切り点 + 目次 + 表 3x3 + 表 %1$dx%2$d + シンプルな表を作成 + 削除 + 複製 + コピー + 貼り付け + リンクを追加 + 他のオブジェクトへのリンクを作成 + 移動 + 移動 + スタイルを消去 + 戻る + 右揃え + 中央揃え + 左揃え + デフォルト + 灰色 + 黄色 + 橙色 + 赤色 + 桃色 + 紫色 + 青色 + 水色 + 青緑色 + 緑色 + %1$sの背景 + + + ウィジェットを作成 + 編集 + 追加 + ウィジェットを追加 + ウィジェットのソース + ウィジェットの種類 + 下に追加 + ツリー表示 + 階層構造で表示します + リンク + コンパクト表示 + リスト形式で表示します + コンパクトなリスト形式で表示します + リスト + ビュー + クエリやコレクションのレイアウトで表示します + コンパクトリスト + ソースを変更 + 種類を変更 + ウィジェットを削除 + 空のごみ箱 + ウィジェットを編集 + ウィジェット %1$sが追加されました + + + タイプ + プロパティ + オブジェクトを整理するための分類がタイプです。 + あらゆる物事にはつながりがあります。プロパティを作成し、オブジェクト同士のつながりを発展させていきましょう。 + タイプを作成 + プロパティを作成 + 作成したタイプ + 作成したプロパティ + ライブラリ + タイプ「%1$s」を作成 + プロパティ「%1$s」を作成 + %1$s「%2$s」が見つかりませんでした + 新しく作成、もしくは検索 + タイプ「%1$s」が見つかりませんでした + タイプ「%1$s」が追加されました + タイプ「%1$s」が削除されました + プロパティ「%1$s」が追加されました + プロパティ「%1$s」が削除されました + 問題が発生しました。もう一度お試しください。 + 新しいタイプ + 完了 + タイプ名 + + + あらゆる情報をここに + 暗号化、オフライン、そしてオープンに。 + 自分だけのデジタル空間で、創作と共同編集を。暗号化、オフライン、そして[オープン](%@)に。 + "続行することで同意したものとみなします。 " + 利用規約 + "及び" + プライバシーポリシー + 初めて利用する + 次へ + すでに鍵を持っています + 自分の鍵を表示する + 招待コードを入力 + お持ちでない場合は、\nanytype.ioにアクセスして、待機リストに登録してください。 + これは保管庫です。 + 作成したデータはすべて暗号化され、安全に保管されます。データはお使いのデバイス内に保存され、分散ネットワークにバックアップされます。 + これがあなたの鍵です + これがログインIDとパスワードの代わりとなります。紛失しないようにご自身で大切に保管してください。あとでアプリの設定画面から確認できます。さらに詳しく + 鍵を表示 + 鍵を表示してコピー + あとで + あとで + クリップボードにコピー + Anytype ID + アカウントを作成中... + あなたの鍵を入力 + または + 保管庫にログイン中 + 鍵を入力してください + 名前を入力 + さらに詳しく + あとでアプリの設定画面から確認できます。 + 鍵とは何ですか? + これはリカバリーフレーズと呼ばれるものです。このデバイス上で魔法のように作られた12個のランダムな単語の組み合わせです。 + この単語の組み合わせを知っている人だけが、保管庫にアクセスすることができます。 + そして今、この組み合わせを知っているのは世界であなただけです。 + たとえ地球上のすべての計算リソースを使ったとしてもアクセスできません。もし鍵を紛失した場合、二度と復元することはできません。そのため、絶対に紛失しないようにしてください! + おすすめの保管方法はありますか? + 最も簡単な方法は、パスワードマネージャーを使うことです。 + 最も安全な方法は、紙に書いて、ネットから切り離した安全な場所に保管することです。 + + + 複製 + ウィジェット作成 + ダウンロード + + + 何も見つかりませんでした + オブジェクトタイプが見つかりませんでした。リクエストを変更してみてください。 + + + 削除ボタン + 複製ボタン + ビュー編集ボタン + プロパティ編集ボタンを開く + このクエリからプロパティを削除 + ページ検索 + ページ間の移動 + 新しくページを追加 + メンションアイコン + リンクアイコンを削除 + リンクアイコンをコピー + オブジェクト画像または絵文字 + チェックボックスアイコン + レイアウトアイコン + + + 了解 + 閉じる + + + ギャラリーはまもなくサポートされます + 現時点ではデスクトップ版をご利用ください + + + 名前を付けて保存 + ノート + ブックマーク + 画像 + ファイル + 画像 + ファイル + 動画 + データ + Anytypeに追加 + Anytypeファイルアップロードエラー: %1$s + すべてのファイルが正常にアップロードされました + 新しいオブジェクトがスペース「%1$s」に追加されました + 追加 + 開く + + + なし + クリップボード + ドラッグ & ドロップ + インポート済みオブジェクト + Web clipper + モバイル共有拡張機能 + 活用例 + インストールされました + ブックマーク + + + %d個のバックリンク + + + %d件のリンク + + + 明日 + 今日 + 昨日 + 過去7日間 + 過去30日間 + さらに古い + %1$s日前 + %1$s日後 + 予備のマルチプレックスライブラリを使用する + ローカルログを共有 + + モバイル版では、まだボードビューに対応していません。\nビューの種類を変更するか(設定 -> ビュー)、新しく作成してください。 + モバイル版では、まだカレンダービューに対応していません。\nビューの種類を変更するか(設定 -> ビュー)、新しく作成してください。 + モバイル版では、まだグラフビューに対応していません。\nビューの種類を変更するか(設定 -> ビュー)、新しく作成してください。 + モバイル版では、まだこのビューに対応していません。\nビューの種類を変更するか(設定 -> ビュー)、新しく作成してください。 + ピン留め + ピン留め + ピン留め解除 + デフォルトに設定 + 左へ移動 + 右へ移動 + クリップボードからオブジェクトを作成 + 問題が発生しました。もう一度お試しください + 「%1$s」を作成 + オプションを作成 + オプションを編集 + 何も見つかりませんでした。開始する最初のオプションを作成してください。 + オプションなし + プロパティが空です + 本当によろしいですか? + このオプションがスペースから完全に削除されます + オブジェクトが見つかりません + 値なし + プロパティが空です + 本当によろしいですか? + オブジェクトがごみ箱に移動されます。 + 名前を入力... + オブジェクトを作成 + このクエリはデスクトップ版でのみ変更できます + + 鍵が正しくありません + ネットワーク + ネットワーク: + Anytype Network + Anytype Networkにバックアップ + ローカルモード + バックアップせず、ローカルネットワークを通して同期 + セルフホスト + セルフホストのネットワークでバックアップ + ネットワーク設定 + トラブルシューティング + タップしてネットワーク設定ファイルを登録 + 現在の設定: %1$s + 種類を表示 + 種類を非表示 + + + 招待リンクを共有 + 招待リンクを共有して、あなたのスペースに招待しましょう。参加リクエストを受け取った後、アクセス権を付与することができます。 + 招待リンク + スペースへ参加 + スペース所有者へのメッセージ + スペース所有者から承認されると、スペースに参加することができます。この際、アクセス権は所有者によって付与されます。 + %1$sに参加 + %1$sに%2$sさんから招待されました。 + スペースに参加 + リクエストを送信しました + スペース所有者から参加リクエストが承認されると通知が届きます。 + スペース管理 + 設定中です。完了までしばらくお待ちください。 + 参加リクエストを送信 + スペース %1$sに%2$sさんが招待してくれました。参加リクエストを送信し、スペース所有者に入れてもらいましょう。 + タップしてメッセージを書く + スペースを共有 + メンバー + これ以上、閲覧権限のメンバーを追加できません。 + これ以上、編集権限のメンバーを追加できません。 + ✦ アップグレード + ✦ アップグレードしてさらに多くのメンバーを追加 + ✦ アップグレードしてさらにスペースを追加 + メンバー + 退出リクエスト + 参加リクエスト + 承認 + リクエストを確認 + 所有者 + 編集可能 + 閲覧可能 + 閲覧のみ許可 + ✦ さらに多くのメンバーに閲覧権限を与える + 編集を許可 + ✦ さらに多くのメンバーに編集権限を与える + 却下 + %1$sさんからスペース「%2$s」への参加リクエストが届きました + メンバーを削除 + %1$sさんがこのスペースにアクセスできなくなります + リクエストを送信しました + 共有を停止 + スペース共有の停止 + メンバーはこのスペースに同期できなくなり、招待リンクが無効になります。 + 詳しく見る + 共有 + 種類 + 共有 + 管理 + スペースから退出 + スペース「%@」はお使いのデバイスから削除され、アクセスできなくなります。 + 招待リンクを作成し、スペースに新しいメンバーを招待しましょう + 招待リンクを作成 + QRコードを表示 + スペースの共有方法 + 一緒に編集したい相手に招待リンクを伝えましょう。 + 一緒に編集したい相手にQRコードを見せましょう。 + リンクがクリックされると、あなたにスペースへの参加リクエストが送信されます。 + 参加リクエストが届いたら承認して、共有相手にアクセス権を設定しましょう。 + 1. + 2. + 3. + 招待リンクの削除 + リンクを削除 + 今後、新しいメンバーがこのスペースに参加できなくなります。新しい招待リンクはいつでも作成できます。 + あなたは既にこのスペースのメンバーです。 + スペースを開く + スペースを削除しました + スペースが見つかりません + 閲覧のみ許可されています。編集するには、スペース所有者にご連絡ください。 + %1$sさんからスペース「%2$s」への参加リクエストが届きました。 + %1$sさんからスペース「%2$s」への退出リクエストが届きました。 + 新しい参加リクエスト + %1$sさんがスペース「%2$s」に閲覧可能権限で参加しました。 + %1$sさんがスペース「%2$s」に編集可能権限で参加しました。 + リクエストが承認されました + スペース「%1$s」への参加リクエストが編集可能権限で承認されました。まもなく、アクセスできるようになります。 + Your request to join the %1$s space has been approved. The space will be available on your device soon. + スペース「%1$s」への参加リクエストが閲覧可能権限で承認されました。まもなく、アクセスできるようになります。 + スペース 「%1$s」にアクセスできなくなりました。 + スペース「%1$s」から退出させられました。スペース所有者によってスペースごと削除された可能性があります。 + 権限が変更されました + あなたのアクセス権が閲覧可能権限にスペース「%1$s」では変更されました。 + あなたのアクセス権が編集可能権限にスペース「%1$s」では変更されました。 + スペース「%1$s」への参加リクエストが拒否されました。 + リクエストが拒否されました + ビュー + スペースに移動 + アクセス権の変更は許可されていません + ストレージ上限に達しました + オブジェクトへのディープリンク + オブジェクトが利用できません。所有者に共有するように依頼してください + スペースでの共同編集 + 1. スペースの設定画面を開く + 2. 共有を開く + 3. 招待リンクを作成して共有する + プロフィールを編集 + + %1$d名のメンバー + + + %1$d件のリクエスト + + あなた + スペース + 参加中 + 有効 + 削除済み + 参加リクエストをキャンセル + もう一度参加リクエストを送信してください + キャンセルしない + 招待が見つかりませんでした。デバイスの同期状況をご確認ください。 + 無効な招待リンクです。URLに間違いがないかご確認ください。 + スペースが削除されたようです。デバイスの同期状況をご確認ください。 + このスペースの上限に達しました。メンバーシップをアップグレードするか、担当者にお問い合わせください。 + スペース共有が停止されたようです。デバイスの同期状況をご確認ください。 + おっと!リクエストの送信に失敗しました。インターネット接続を確認し、もう一度お試しください。 + このリンクは利用できません + この招待リンクは無効です。スペースが見つかりませんでした。 + リクエストを送信しました + + + メンバーシップ + Anytype Networkに参加して、共に未来を創りましょう + 未来を共に創造する + 大切なメンバーであるあなたの声を反映させましょう。限定イベントや重要な方針決定へのご参加を通じて、私たちと共に目指すべき未来を創り上げましょう。 + メンバーシップ特典 + より多くのバックアップ容量、同期上限の緩和、共有スペースへの招待数増加、Anytype Networkでの独自IDをご利用いただけます。 + デジタルの自由を共に守る + あなたの支援が私たちの活動を支え、ユーザー主導の安全で共に発展するデジタルネットワークという私たちの理念の実現を後押しします。 + つながりに、もっと力を + 一人ひとりのつながりが、個々の力だけでは成し得ない価値を生み出します。あなたのメンバーシップが、私たちのネットワークの持続的な成長を支える基盤となります。 + Explorer + 同期や自動バックアップ、複数人での共同編集ができます + Builder + 複数人での共同作業とストレージ容量を増加させましょう + Co-Creator + 私たちの活動を応援し、限定コンテンツや特別な特典を入手しましょう + カスタム + ご希望に沿ったプランをご提供します + さらに詳しく + 支払いの管理 + 連絡先 + 詳しく見る + 送信 + クレジットカードで支払う + メールアドレスの変更 + メンバーシッププランの詳細 + プライバシーポリシー + 利用規約 + Anytypeを仕事や学習などでご利用されますか? + こちらからお問い合わせください。 + 現在 + + 自分だけの名前を決めましょう + これはAnytype Network内で使えるあなただけの名前です。メンバーシップの加入認証にも使われます。自分専用のドメイン名として使うことができ、変更できません。 + あなた自身 + 最低7文字 + この名前は既に使用されています。 + 「%1$s」は利用可能です! + 名前を検証中 + 無期限 + %1$sまで有効 + 同期や自動バックアップ、複数人での共同編集ができます + + 1 GBのネットワークストレージ + 10個までの1対1のスペース + 最大10個の閲覧限定の共有スペース + + 複数人での共同作業とストレージ容量を増加させましょう + + あなただけの名前(7文字以上) + 128 GBのネットワークストレージ + 25名の編集メンバー + 優先サポート + + 私たちの活動を応援し、限定コンテンツや特別な特典を入手しましょう + + あなただけの名前(5文字以上) + 256GBのネットワークストレージ + 25名の編集メンバー + チームとのチャット + 限定コレクション + + クレジットカードでの支払い + 送信 + 特典内容 + 最新情報や無料特典を受け取りましょう! + あなたの保管庫と結びつけられることは一切ありません。 + 最新情報や無料特典を受け取る! + メールアドレス + 加入状況: + 有効期限 + 支払方法 + Stripe + 暗号資産 + Appleサブスクリプション + Google Playでの定期購入 + 支払いの管理 + プラン変更は、デスクトップ版から行ってください + プラン変更は、iOS版から行ってください + プラン変更は、別のGoogleアカウントで行ってください + メールアドレスの変更 + + メールアドレスに送信されたコードを入力 + 再送信 + %1$d秒後に再送信 + + ネットワーク %1$sへようこそ + ご興味をお持ちいただき、ありがとうございます! + 続行 + + support@anytype.io + メンバーシップ画面エラーレポート - %1$s + Anytypeチーム担当者の方へ\n\n + + アプリ使用中に、メンバーシップ画面にて、エラーが表示されました。詳しくは以下の通りです。:\n + + - エラーメッセージ: %1$s\n + - 状況説明: \n + - 発生日時: %2$s\n + - お使いのデバイス名: %3$s\n + - お使いのOS: %4$s\n + - アプリバージョン: %5$s\n + + 追記:\n\n + + 本件の解決にご協力いただき、ありがとうございます。\n\n + + よろしくお願いいたします。 \n + + membership-upgrade@anytype.io + %1$sへのアップグレード + Anytypeチーム担当者の方へ\n現在利用しておりますAnytypeのメンバーシップについて、変更したくご連絡いたしました。\n(下記のオプションの中から必要なものをお選びいただき、詳細をご記入ください)\n +\n +・リモートストレージの追加\n +・スペース編集者の追加\n +・共有スペース数の追加\n +\n +具体的にご希望の内容がございましたら、こちらにご記載ください。 + + 入力中に問題が発生しました。もう一度お試しください + エラーが発生しました。しばらくしてからもう一度お試しください。 + 接続できません。インターネット接続を確認してください + 使用できない文字が含まれています + ログインしていません! 😵‍💫 + エラーが発生しました。しばらくしてからもう一度お試しください。 + このプランでは名前は使用できません + プランが見つかりませんでした。あとでもう一度お試しください + 名前が長すぎます + 名前が短すぎます + この名前は予約できません + エンプティエラー + 不明なエラー + 正しくメールアドレスを入力してください + このメールアドレスは確認済みです + メールは既に送信されています。1分後にもう一度お試しください + メールの送信に失敗しました + 既にメンバーシップに加入しています + コードの有効期限が切れました + コードが間違っています + コードが一致しました + メンバーシップが見つかりませんでした + すでにメンバーシップに加入しています + + 不明なエラーが発生しました。しばらくしてから再度お試しください。 + 無効な入力が検出されました。データを確認して、もう一度やり直してください。 + 内部エラーが発生しました。開発チームに報告されました。 + インポートできる項目がありませんでした。正しいオブジェクトを選択してください。 + ユーザーによってインポートがキャンセルされました + 行またはプロパティのインポート件数が上限を超えています。量を減らしてやり直してください。 + ファイルの読み込み中にエラーが発生しました。ファイルを確認してもう一度お試しください。 + このアクションを実行するために必要な権限がありません。権限を確認して、もう一度やり直してください。 + + 作成者 + インポート + 新しいスペースにインポート + インポート完了 + 新しいスペースに「%1$s」がインポートされました。スペースを開いて使い始めましょう。 + おっと! + スペースを開く + 閉じる + 参加 + 通知を受け取る + あとで + スペースへの参加や退出のリクエストがあった際に通知を受け取るには、通知を有効にしてください。 + 有効化 + クリップボードからブックマークを作成 + エラー: 予期しないレイアウト + エラー: オブジェクトIDがありません + 保留中... + %1$sごと + 無料期間 %1$s + 無料期限... + 無料期限 + 有効期限 無期限 + 有効期限... + 続行することで同意したものとみなします。 + 利用規約 + 及び + プライバシーポリシー + 他のプラットフォームにて、既にメンバーシップにご加入いただいているようです。 + 異なるIDの契約が見つかりました + 複数の契約が見つかりました + スペースメンバーエラー + リクエストメンバーエラー + 現在のユーザーステータスエラー + 現在のメンバーシップステータスエラー + メンバーシップをアップグレード + ストレージ容量の追加、スペースエディターに関するご質問、または共有スペースの増設については、お気軽にお問い合わせください。Anytypeがお客様のニーズに合わせた詳細と条件をご案内いたします。 + Anytypeへのお問い合わせ + 共有スペースの上限数である%1$dつに達しました。 + + %d 年間 + + + %d ヶ月 + + + %d 週間 + + + %d 日間 + + お問い合わせ + はい + ネットワークIDの不一致が検出されました。ネットワーク設定を確認してください。 + 保管庫選択エラーです。 + このアカウントは削除されました。別のアカウントを使用するか、新しいアカウントを作成してください。 + アカウントを取得できません。Anytypeアプリを最新版に更新してください。 + "関連: " + 関連するオブジェクトが見つかりません + 未入力の値を表示 + 下に表示 + 上に表示 + すべてのオブジェクトを表示 + 同期状況取得エラー + ローカルモード + バックアップが無効です + セルフホスト + 同期完了 + + %1$d件を同期中... + + P2P接続 + + %1$dつのデバイスに接続済み + + 接続できません + 制限されています。デバイスの設定を確認してください。 + 接続されていません + Anytype Network + エンドツーエンド暗号化 + + %1$d件を同期中... + + ネットワーク接続中... + 接続なし + 同期が遅くなる可能性があります。アプリを更新してください。 + ストレージ上限に達しました + 互換性のないバージョン + スペースにアクセスできません + 不明なエラー + 変更履歴 + 変更が見つかりませんでした + 変更履歴取得エラー + メンバー取得エラー + 新しいオブジェクト + 自分のスペース + すべてのオブジェクト + リンクのない + グラフビューで、どのオブジェクトともリンクやバックリンクなどの繋がりのないオブジェクトです。 + ごみ箱を表示 + ページ + リスト + メディア + ブックマーク + ファイル + タイプ + プロパティ + 並び替え + Z → A + A → Z + 新しい順 + 古い順 + 更新日付順 + 作成日時順 + 最後に使用した日時順 + 名前 + すべてのオブジェクト + 今日 + 昨日 + 過去7日間 + 過去14日間 + 保管庫ではスペースを一元管理することができます。\n作成、参加、共有など、すべての操作がここから安全に行えます。 + ドラッグ&ドロップで、スペースの並び順を変えられるようになりました。これにより、スペース間の移動がさらに楽になります。 + スペースの背景には、ぼかしたアイコンが表示されるようになりました。スペースの設定画面から変更することができます。 + 保管庫へようこそ + + 何もありません。 + 最初のプロジェクトを作成して、使い始めましょう。 + 該当する結果が見つかりませんでした。 + 別のキーワードで検索してください。 + 問題が発生しました。 + 初めてのマイスペース + エラー: 予期しないレイアウト + プロパティが追加されました + プロパティが削除されました + タイプが追加されました + タイプが削除されました + アーカイブされました + 新しいプロパティ + 新しいタイプ + グラフビューで、どのオブジェクトともリンクやバックリンクなどの繋がりのないオブジェクトです。 + をゴミ箱に移動しました + 戻るボタン + 共有とメンバーボタン + ホームボタン + オブジェクト検索ボタン + オブジェクト作成ボタン + チャット + コピー + テキストをコピー + メッセージを編集 + 編集済み + メッセージはありません。\n会話を始めましょう。 + メッセージを入力... + メンション + この日付にはまだ何もありません + 選択した日付は期間外です。(年 %1$d から %2$d)。この期間内で日付を選択してください。 + 作成者 + 既存のオブジェクトを選択 + ファイルをアップロード + メディアをアップロード + アップロード + 返信 + リアクションを追加 + 何もありません。 + 最初のプロジェクトを作成して、使い始めましょう。 + 完全に削除 + クエリを開く + クエリを作成 + デフォルトに設定 + 編集 + 複製 + 削除 + ヘッダー + プロパティパネル + 非表示 + ファイル + オブジェクト内で見つかりました + まだプロパティがありません + プロパティ + 編集中のタイプ + 項目を編集 + 新しい項目 + 項目をプレビュー + 保存 + タイプからリンク解除 + タイプから削除 + ゴミ箱に移動 + 現在のタイプに追加 + 削除 + オブジェクト内の個別プロパティ + タイプに存在しないプロパティです。タイプに追加することで、他のオブジェクトにもこのプロパティを表示させることができます。 + 了解 + このタイプには\nテンプレートがありません + 個別 + 非表示 + 個別プロパティ + このオブジェクトにのみ存在するプロパティです。タイプに追加することで、他のオブジェクトでも使うことができます。 + まだプロパティがありません。 + タイプを開けませんでした + このタイプの編集は許可されていません。 + + + + + + + + ファイル + 添付ファイル + まずは最初のスペースを作成しましょう + 何もありません。 + 表情と人物 + 動物と自然 + 食べ物と飲み物 + アクティビティとスポーツ + 旅行と場所 + オブジェクト + 記号 + + 最近使用された + まだリアクションがありません + 恐らく、誰かによってリアクションが削除されたか、技術的な問題が発生しました。 + 連絡先 + タイプを削除 + あとから復元することはできません。 + このメッセージを削除してもよろしいですか? + クエリ + 更新が進行中 + これには数分かかることがあります。完了するまでアプリを閉じずに、そのままお待ちください。 + もう一度実行 + 移行失敗 + 約%1$sMBの空き容量を確保し、再度実行してください。 + プロパティ + テキスト + 入力 + 数字 + 入力 + 日付 + + URL + 追加 + メールアドレス + 入力 + 電話番号 + 入力 + セレクト + セレクト + 複数セレクト + セレクト + オブジェクト + 追加 + ファイルとメディア + 追加 + セレクト + 現在のタイプに追加 + オブジェクトから削除 + 名前を入力 + 説明を追加 + 共同編集 + ライブラリ + 環境設定 + データ管理 + その他 + 招待 + メンバーを招待 + QRコード + メンバー + タイプ + デフォルトのタイプ + 壁紙 + リモートストレージ + スペース情報 + スペースを削除 + 画像を編集 + 保存 + プロパティを追加 + プロパティ「%1$s」を作成 + プロパティ種類 + 既存のプロパティ + 検索、もしくは新しく作成 + 新しいプロパティ + オブジェクト数制限 + オブジェクト数を制限 + すべて + 新しいプロパティ作成中にエラーが発生しました + プロパティ更新中にエラーが発生しました + プロパティをタイプに追加中にエラーが発生しました + プロパティの種類を選択 + スペースから削除 + タイプからリンク解除 + オブジェクト数を制限 + 時間を含める + 上記を理解した上で、この保管庫を削除することを希望します。 + 最新版への更新 + 新しいチャット機能が登場します。メッセージ数の表示や通知など、便利な機能が追加されます。完了まで少々お時間をいただきますが、常にデータは安全に保管されます。 + 少々お時間をいただきますが、データは安全です。 + 更新開始 + さらに詳しく + 更新の流れ + あなたのデータは常に安全に保護されています + アップデート中もデータは完全に保護されます。アップデートはお使いのデバイス上で直接行われ、同期済みのデータには影響ありません。新しいデータ形式への移行と同時に、以前の形式のバックアップがお使いのデバイス上に自動的に作成されます。 + このバックアップは、予期せぬ事態が起こった際の原因究明やお客様のサポートに役立てられます。 + 更新中は読み込み画面が表示されます。完了後、通常通りアプリをご利用いただけます。 + アイコンを変更 + 削除 + 検索... + 招待リンクがコピーされました + タイプを作成 + タイプ名を変更 + タイプ名 (複数形) + プロジェクトなど + Projectsなど + タイプに合わせてリセット + タイプとは異なるレイアウトが設定されています。タイプに合わせてリセットしますか? + デフォルトに戻す + タイプに合わせてリセット + 提案 + まだウィジェットがありません + タイプウィジェットの自動作成 + タイプ + 作成したタイプ + 標準タイプ + プロパティ + 作成したプロパティ + 標準プロパティ + ゴミ箱に移動 + アカウント作成中にエラーが発生しました: スペースがありません + あなたの名前を設定しましょう + 何かを共有した相手にのみ表示されます。入力された名前が一元管理されることはありません。 + 最新情報を受け取る + 便利な使い方や活用のコツ、製品の更新情報などをお届けします。ご登録いただいたメールアドレスが個人を特定する情報と関連付けられることはありません。お客様のデータが第三者に共有されることは一切ございません。 + メールアドレスを入力 + 続行 + あとで + メールアドレスが正しくありません。 + 名前を入力 + プッシュ通知を有効化 + スペースでメッセージやメンションがあった場合にすぐに通知を受け取ります。 + 通知を有効化 + あとで + diff --git a/localization/src/main/res/values-nl-rNL/strings.xml b/localization/src/main/res/values-nl-rNL/strings.xml index d10f682232..0e4a5d973a 100644 --- a/localization/src/main/res/values-nl-rNL/strings.xml +++ b/localization/src/main/res/values-nl-rNL/strings.xml @@ -1234,6 +1234,7 @@ %1$s heeft zich aangesloten bij de ruimte %2$s met bewerkingsrechten Verzoek goedgekeurd Je verzoek om deel te nemen aan de ruimte %1$s is goedgekeurd met alleen-lezen toegangsrechten. De ruimte zal binnenkort beschikbaar zijn op je apparaat. + Your request to join the %1$s space has been approved. The space will be available on your device soon. Je verzoek om deel te nemen aan de ruimte %1$s is goedgekeurd met bewerktoegangsrechten. De ruimte zal binnenkort beschikbaar zijn op je apparaat. De ruimte \"%1$s\" is niet langer toegankelijk. Je bent verwijderd uit de ruimte \"%1$s\" of de ruimte is verwijderd door de eigenaar. diff --git a/localization/src/main/res/values-no-rNO/strings.xml b/localization/src/main/res/values-no-rNO/strings.xml index b7c4ab5491..9e9b3669ff 100644 --- a/localization/src/main/res/values-no-rNO/strings.xml +++ b/localization/src/main/res/values-no-rNO/strings.xml @@ -1234,6 +1234,7 @@ %1$s joined %2$s space with edit access rights Request approved Your request to join the %1$s space has been approved with read-only access rights. The space will be available on your device soon. + Your request to join the %1$s space has been approved. The space will be available on your device soon. Your request to join the %1$s space has been approved with edit access rights. The space will be available on your device soon. The space \"%1$s\" is no longer accessible. You have been removed from the space \"%1$s\", or the space was deleted by the owner. diff --git a/localization/src/main/res/values-pt-rBR/strings.xml b/localization/src/main/res/values-pt-rBR/strings.xml index f3771fcb39..d5cf02c3b1 100644 --- a/localization/src/main/res/values-pt-rBR/strings.xml +++ b/localization/src/main/res/values-pt-rBR/strings.xml @@ -1234,6 +1234,7 @@ %1$s Juntou-se a %2$s de espaço com direitos de edição de acesso Solicitação aprovada Seu pedido para participar do espaço %1$s foi aprovado com direitos de acesso somente, leitura. O espaço estará disponível no seu dispositivo em breve. + Your request to join the %1$s space has been approved. The space will be available on your device soon. Seu pedido para ingressar no espaço %1$s foi aprovado com direitos de acesso para edição. O espaço estará disponível no seu dispositivo em breve. The space \"%1$s\" is no longer accessible. You have been removed from the space \"%1$s\", or the space was deleted by the owner. diff --git a/localization/src/main/res/values-ru-rRU/strings.xml b/localization/src/main/res/values-ru-rRU/strings.xml index 37f7b7aa0a..adfcd74435 100644 --- a/localization/src/main/res/values-ru-rRU/strings.xml +++ b/localization/src/main/res/values-ru-rRU/strings.xml @@ -1242,6 +1242,7 @@ %1$s присоединился %2$s к пространству с правами доступа редактирования Запрос одобрен Ваш запрос на присоединение к пространству %1$s был одобрен с правами доступа только для чтения. Пространство будет доступно на вашем устройстве в ближайшее время. + Your request to join the %1$s space has been approved. The space will be available on your device soon. Ваш запрос на присоединение к пространству %1$s был одобрен с правами на редактирование. Пространство будет доступно в ближайшее время на устройстве. Пространство \"%1$s\" больше недоступно. Вы были удалены из пространства \"%1$s\", или оно было удалено владельцем. diff --git a/localization/src/main/res/values-tr-rTR/strings.xml b/localization/src/main/res/values-tr-rTR/strings.xml index 4c5745be06..70ee200210 100644 --- a/localization/src/main/res/values-tr-rTR/strings.xml +++ b/localization/src/main/res/values-tr-rTR/strings.xml @@ -1234,6 +1234,7 @@ %1$s, %2$s alanına düzenleme erişim haklarıyla katıldı. Talep kabul edildi %1$s alanına katılma isteğiniz salt okunur erişim haklarıyla onaylandı. Alan yakında cihazınızda kullanılabilir olacak. + Your request to join the %1$s space has been approved. The space will be available on your device soon. %1$s alanına katılma isteğiniz düzenleme erişim haklarıyla onaylandı. Alan yakında cihazınızda kullanılabilir olacak. \"%1$s\" alanı artık erişilebilir değil. \"%1$s\" alanından çıkarıldınız veya alan sahibi tarafından silindi. @@ -1586,7 +1587,7 @@ Lütfen ihtiyaçlarınızla ilgili özel ayrıntıları burada belirtin.Nesne oluştur butonu Sohbet Kopyala - Copy Text + Metni Kopyala Mesajı düzenle düzenlendi Burada henüz bir mesaj yok.\nTartışma başlatan ilk kişi siz olun. diff --git a/localization/src/main/res/values-uk-rUA/strings.xml b/localization/src/main/res/values-uk-rUA/strings.xml index aba05a01ae..bf40664106 100644 --- a/localization/src/main/res/values-uk-rUA/strings.xml +++ b/localization/src/main/res/values-uk-rUA/strings.xml @@ -1242,6 +1242,7 @@ %1$s joined %2$s space with edit access rights Request approved Your request to join the %1$s space has been approved with read-only access rights. The space will be available on your device soon. + Your request to join the %1$s space has been approved. The space will be available on your device soon. Your request to join the %1$s space has been approved with edit access rights. The space will be available on your device soon. The space \"%1$s\" is no longer accessible. You have been removed from the space \"%1$s\", or the space was deleted by the owner. diff --git a/localization/src/main/res/values-zh-rCN/strings.xml b/localization/src/main/res/values-zh-rCN/strings.xml index ae198fba15..f18e2d60de 100644 --- a/localization/src/main/res/values-zh-rCN/strings.xml +++ b/localization/src/main/res/values-zh-rCN/strings.xml @@ -1230,6 +1230,7 @@ %1$s 加入了 %2$s 空间并拥有编辑访问权限 请求已批准 您加入 %1$s 空间的请求已获批准并获得了只读访问权限,该空间很快就会在您的设备中显示。 + Your request to join the %1$s space has been approved. The space will be available on your device soon. 您加入 %1$s 空间的请求已获批准并获得了编辑访问权限,该空间很快就会在您的设备中显示。 空间“%1$s”已无法访问。 “%1$s”空间已移除了您,或者此空间已被所有者删除。 diff --git a/localization/src/main/res/values-zh-rTW/strings.xml b/localization/src/main/res/values-zh-rTW/strings.xml index 291c7f2934..8bad7eb47e 100644 --- a/localization/src/main/res/values-zh-rTW/strings.xml +++ b/localization/src/main/res/values-zh-rTW/strings.xml @@ -1230,6 +1230,7 @@ %1$s 擁有編輯存取權限並加入了 %2$s 空間 請求已批准 您請求加入的 %1$s 空間已獲得批准,並具有唯讀存取權限。 該空間很快就可以在您的裝置上使用。 + Your request to join the %1$s space has been approved. The space will be available on your device soon. 您請求加入的 %1$s 空間已獲得批准,並具有編輯存取權限。 該空間很快就可以在您的裝置上使用。 The space \"%1$s\" is no longer accessible. You have been removed from the space \"%1$s\", or the space was deleted by the owner. diff --git a/localization/src/main/res/values/strings.xml b/localization/src/main/res/values/strings.xml index 635f825f4e..9203301776 100644 --- a/localization/src/main/res/values/strings.xml +++ b/localization/src/main/res/values/strings.xml @@ -1430,6 +1430,7 @@ %1$s joined %2$s space with edit access rights Request approved Your request to join the %1$s space has been approved with read-only access rights. The space will be available on your device soon. + Your request to join the %1$s space has been approved. The space will be available on your device soon. Your request to join the %1$s space has been approved with edit access rights. The space will be available on your device soon. The space \"%1$s\" is no longer accessible. You have been removed from the space \"%1$s\", or the space was deleted by the owner. 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 24f21b3fc8..9f711e3a26 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 @@ -1350,7 +1350,18 @@ class HomeScreenViewModel( ) } is Event.Command.Details -> { - curr = curr.copy(details = curr.details.process(e)) + if (e is Event.Command.Details.Amend) { + val hasTargetKeyValueChanges = Widget.Source.SOURCE_KEYS.any { key -> + key in e.details.keys + } + if (hasTargetKeyValueChanges) { + curr = curr.copy(details = curr.details.process(e)) + } else { + Timber.d("Widget source reducer: Ignoring Amend event: no relevant keys in ${e.details.keys}") + } + } else { + curr = curr.copy(details = curr.details.process(e)) + } } is Event.Command.LinkGranularChange -> { curr = curr.copy( diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/main/MainViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/main/MainViewModel.kt index 9ea02691c4..2bfa5e1ed1 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/main/MainViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/main/MainViewModel.kt @@ -23,6 +23,7 @@ import com.anytypeio.anytype.domain.auth.model.AuthStatus import com.anytypeio.anytype.domain.base.BaseUseCase import com.anytypeio.anytype.domain.base.Interactor import com.anytypeio.anytype.domain.config.ConfigStorage +import com.anytypeio.anytype.domain.deeplink.PendingIntentStore import com.anytypeio.anytype.domain.misc.DeepLinkResolver import com.anytypeio.anytype.domain.misc.LocaleProvider import com.anytypeio.anytype.domain.multiplayer.SpaceInviteResolver @@ -69,7 +70,8 @@ class MainViewModel( private val globalSubscriptionManager: GlobalSubscriptionManager, private val spaceInviteResolver: SpaceInviteResolver, private val spaceManager: SpaceManager, - private val spaceViews: SpaceViewSubscriptionContainer + private val spaceViews: SpaceViewSubscriptionContainer, + private val pendingIntentStore: PendingIntentStore ) : ViewModel(), NotificationActionDelegate by notificationActionDelegate, DeepLinkToObjectDelegate by deepLinkToObjectDelegate { @@ -308,8 +310,34 @@ class MainViewModel( } } - fun onNewDeepLink(deeplink: DeepLinkResolver.Action) { + fun handleNewDeepLink(deeplink: DeepLinkResolver.Action) { deepLinkJobs.cancel() + viewModelScope.launch { + checkAuthorizationStatus(Unit).process( + failure = { Timber.e(it, "Failed to check authentication status") }, + success = { authStatus -> processDeepLinkBasedOnAuth(authStatus, deeplink) } + ) + } + } + + private fun processDeepLinkBasedOnAuth( + authStatus: AuthStatus, + deeplink: DeepLinkResolver.Action + ) { + if (authStatus == AuthStatus.UNAUTHORIZED && deeplink is DeepLinkResolver.Action.Invite) { + saveInviteDeepLinkForLater(deeplink) + } else { + Timber.d("Proceeding with deeplink: $deeplink") + launchDeepLinkProcessing(deeplink) + } + } + + private fun saveInviteDeepLinkForLater(deeplink: DeepLinkResolver.Action.Invite) { + pendingIntentStore.setDeepLinkInvite(deeplink.link) + Timber.d("Saved invite deeplink for later processing: ${deeplink.link}") + } + + private fun launchDeepLinkProcessing(deeplink: DeepLinkResolver.Action) { deepLinkJobs += viewModelScope.launch { awaitAccountStartManager .awaitStart() diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/main/MainViewModelFactory.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/main/MainViewModelFactory.kt index cc92de847e..5f1deb90c1 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/main/MainViewModelFactory.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/main/MainViewModelFactory.kt @@ -9,6 +9,7 @@ import com.anytypeio.anytype.domain.auth.interactor.CheckAuthorizationStatus import com.anytypeio.anytype.domain.auth.interactor.Logout import com.anytypeio.anytype.domain.auth.interactor.ResumeAccount import com.anytypeio.anytype.domain.config.ConfigStorage +import com.anytypeio.anytype.domain.deeplink.PendingIntentStore import com.anytypeio.anytype.domain.misc.LocaleProvider import com.anytypeio.anytype.domain.multiplayer.SpaceInviteResolver import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer @@ -42,7 +43,8 @@ class MainViewModelFactory @Inject constructor( private val globalSubscriptionManager: GlobalSubscriptionManager, private val spaceInviteResolver: SpaceInviteResolver, private val spaceManager: SpaceManager, - private val spaceViews: SpaceViewSubscriptionContainer + private val spaceViews: SpaceViewSubscriptionContainer, + private val pendingIntentStore: PendingIntentStore ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create( @@ -66,6 +68,7 @@ class MainViewModelFactory @Inject constructor( globalSubscriptionManager = globalSubscriptionManager, spaceInviteResolver = spaceInviteResolver, spaceManager = spaceManager, - spaceViews = spaceViews + spaceViews = spaceViews, + pendingIntentStore = pendingIntentStore ) as T } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/multiplayer/RequestJoinSpaceViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/multiplayer/RequestJoinSpaceViewModel.kt index 0d1844ae5b..1fb2552d50 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/multiplayer/RequestJoinSpaceViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/multiplayer/RequestJoinSpaceViewModel.kt @@ -7,6 +7,9 @@ import com.anytypeio.anytype.analytics.base.Analytics import com.anytypeio.anytype.analytics.base.EventsDictionary.screenInviteRequest import com.anytypeio.anytype.analytics.base.EventsDictionary.screenRequestSent import com.anytypeio.anytype.analytics.base.sendEvent +import com.anytypeio.anytype.core_models.Notification +import com.anytypeio.anytype.core_models.NotificationPayload +import com.anytypeio.anytype.core_models.NotificationStatus import com.anytypeio.anytype.core_models.multiplayer.MultiplayerError import com.anytypeio.anytype.core_models.multiplayer.SpaceInviteError import com.anytypeio.anytype.core_models.multiplayer.SpaceInviteView @@ -27,6 +30,7 @@ import com.anytypeio.anytype.domain.workspace.SpaceManager import com.anytypeio.anytype.presentation.common.BaseViewModel import com.anytypeio.anytype.presentation.common.TypedViewState import javax.inject.Inject +import kotlin.random.Random import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -130,54 +134,86 @@ class RequestJoinSpaceViewModel( } fun onRequestToJoinClicked() { - when(val curr = state.value) { - is TypedViewState.Success -> { - joinSpaceRequestJob?.cancel() - joinSpaceRequestJob = viewModelScope.launch { - val fileKey = spaceInviteResolver.parseFileKey(params.link) - val contentId = spaceInviteResolver.parseContentId(params.link) - if (contentId != null && fileKey != null) { - isRequestInProgress.value = true - sendJoinSpaceRequest.async( - SendJoinSpaceRequest.Params( - space = curr.data.space, - network = configStorage.getOrNull()?.network, - inviteFileKey = fileKey, - inviteContentId = contentId - ) - ).fold( - onFailure = { e -> - Timber.e(e, "Error while sending space join request") - if (e is MultiplayerError.Generic) { - commands.emit(Command.ShowGenericMultiplayerError(e)) - } else { - sendToast(e.msg()) - } - }, - onSuccess = { - analytics.sendEvent(eventName = screenRequestSent) - if (notificator.areNotificationsEnabled) { - if (!curr.data.withoutApprove) { - commands.emit(Command.Toast.RequestSent) - } - commands.emit(Command.Dismiss) - } else { - if (!curr.data.withoutApprove) { - commands.emit(Command.Toast.RequestSent) - } - showEnableNotificationDialog.value = true - } - } - ) - isRequestInProgress.value = false - } - } - } else -> { - // Do nothing. + val currentState = state.value + if (currentState !is TypedViewState.Success) return + + joinSpaceRequestJob?.cancel() + joinSpaceRequestJob = viewModelScope.launch { + val fileKey = spaceInviteResolver.parseFileKey(params.link) + val contentId = spaceInviteResolver.parseContentId(params.link) + + if (fileKey == null || contentId == null) { + Timber.w("Could not parse invite link in onRequestToJoinClicked: ${params.link}") + return@launch + } + + isRequestInProgress.value = true + + val params = SendJoinSpaceRequest.Params( + space = currentState.data.space, + network = configStorage.getOrNull()?.network, + inviteFileKey = fileKey, + inviteContentId = contentId + ) + + sendJoinSpaceRequest.async(params).fold( + onFailure = { handleJoinRequestFailure(it) }, + onSuccess = { handleJoinRequestSuccess(currentState.data) } + ) + + isRequestInProgress.value = false + } + } + + private suspend fun handleJoinRequestFailure(error: Throwable) { + Timber.e(error, "Error while sending space join request") + when (error) { + is MultiplayerError.Generic -> commands.emit(Command.ShowGenericMultiplayerError(error)) + else -> sendToast(error.msg()) + } + } + + private suspend fun handleJoinRequestSuccess(data: SpaceInviteView) { + analytics.sendEvent(eventName = screenRequestSent) + + val shouldNotify = data.withoutApprove + val notificationsEnabled = notificator.areNotificationsEnabled + + if (shouldNotify) { + sendApprovalNotification(data) + } + + if (notificationsEnabled) { + if (!shouldNotify) { + commands.emit(Command.Toast.RequestSent) } + commands.emit(Command.Dismiss) + } else { + if (!shouldNotify) { + commands.emit(Command.Toast.RequestSent) + } + showEnableNotificationDialog.value = true } } + private fun createApprovalNotification(data: SpaceInviteView): Notification { + return Notification( + id = Random.nextInt().toString(), + createTime = System.currentTimeMillis(), + status = NotificationStatus.CREATED, + isLocal = true, + payload = NotificationPayload.ParticipantRequestApproved( + spaceId = data.space, + spaceName = data.spaceName + ), + space = data.space + ) + } + + private fun sendApprovalNotification(data: SpaceInviteView) { + notificator.notify(createApprovalNotification(data)) + } + fun onCancelJoinSpaceRequestClicked() { joinSpaceRequestJob?.cancel() isRequestInProgress.value = false diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/multiplayer/ShareSpaceViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/multiplayer/ShareSpaceViewModel.kt index 1a94beab7a..bfb3c8005f 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/multiplayer/ShareSpaceViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/multiplayer/ShareSpaceViewModel.kt @@ -416,23 +416,39 @@ class ShareSpaceViewModel( } fun onRemoveMemberAccepted(identity: Id) { - Timber.d("onRemoveMemberAccepted") + Timber.d("onRemoveMemberAccepted: Starting member removal process for identity: $identity") viewModelScope.launch { - removeSpaceMembers.async( - RemoveSpaceMembers.Params( - space = vmParams.space, - identities = listOf(identity) + try { + removeSpaceMembers.async( + RemoveSpaceMembers.Params( + space = vmParams.space, + identities = listOf(identity) + ) + ).fold( + onFailure = { e -> + Timber.e( + e, + "Error while removing space member (identity: $identity, space: ${vmParams.space})" + ) + when (e) { + is java.net.SocketTimeoutException, + is java.net.UnknownHostException, + is java.io.IOException -> { + sendToast("Network error occurred. Please check your connection and try again.") + } + + else -> proceedWithMultiplayerError(e) + } + }, + onSuccess = { + Timber.d("Successfully removed space member (identity: $identity, space: ${vmParams.space})") + analytics.sendEvent(eventName = removeSpaceMember) + } ) - ).fold( - onFailure = { e -> - Timber.e(e, "Error while removing space member") - proceedWithMultiplayerError(e) - }, - onSuccess = { - Timber.d("Successfully removed space member") - analytics.sendEvent(eventName = removeSpaceMember) - } - ) + } catch (e: Exception) { + Timber.e(e, "Unexpected error while removing space member") + sendToast("An unexpected error occurred. Please try again.") + } } } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/notifications/NotificationsViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/notifications/NotificationsViewModel.kt index 3304dbdbc1..8c0d46718c 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/notifications/NotificationsViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/notifications/NotificationsViewModel.kt @@ -80,7 +80,8 @@ class NotificationsViewModel( notification = notification.id, space = payload.spaceId, spaceName = payload.spaceName, - isReadOnly = !payload.permissions.isOwnerOrEditor() + isReadOnly = payload.permissions == null + || payload.permissions?.isOwnerOrEditor() != true ) } is NotificationPayload.ParticipantRemove -> { diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/notifications/PushKeyProviderImpl.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/notifications/PushKeyProvider.kt similarity index 100% rename from presentation/src/main/java/com/anytypeio/anytype/presentation/notifications/PushKeyProviderImpl.kt rename to presentation/src/main/java/com/anytypeio/anytype/presentation/notifications/PushKeyProvider.kt diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/onboarding/signup/OnboardingMnemonicViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/onboarding/signup/OnboardingMnemonicViewModel.kt index 17a7ea06f4..d03ddfa5d5 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/onboarding/signup/OnboardingMnemonicViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/onboarding/signup/OnboardingMnemonicViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.anytypeio.anytype.analytics.base.Analytics import com.anytypeio.anytype.analytics.base.EventsDictionary +import com.anytypeio.anytype.analytics.base.EventsDictionary.ClickOnboardingButton import com.anytypeio.anytype.core_models.DeviceNetworkType import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.NetworkMode @@ -16,6 +17,7 @@ import com.anytypeio.anytype.domain.network.NetworkModeProvider import com.anytypeio.anytype.presentation.extension.sendAnalyticsOnboardingClickEvent import com.anytypeio.anytype.presentation.extension.sendAnalyticsOnboardingScreenEvent import com.anytypeio.anytype.presentation.extension.sendOpenAccountEvent +import com.anytypeio.anytype.domain.deeplink.PendingIntentStore import javax.inject.Inject import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -27,7 +29,8 @@ class OnboardingMnemonicViewModel @Inject constructor( private val analytics: Analytics, private val configStorage: ConfigStorage, private val networkModeProvider: NetworkModeProvider, - private val networkConnectionStatus: NetworkConnectionStatus + private val networkConnectionStatus: NetworkConnectionStatus, + private val pendingIntentStore: PendingIntentStore ) : ViewModel() { val state = MutableStateFlow(State.Idle("")) @@ -35,9 +38,7 @@ class OnboardingMnemonicViewModel @Inject constructor( init { Timber.i("OnboardingMnemonicViewModel, init") - viewModelScope.sendAnalyticsOnboardingScreenEvent(analytics, - EventsDictionary.ScreenOnboardingStep.PHRASE - ) + sendScreenViewAnalytics() viewModelScope.launch { proceedWithMnemonicPhrase() } @@ -47,92 +48,73 @@ class OnboardingMnemonicViewModel @Inject constructor( if (state.value is State.Mnemonic) { state.value = State.MnemonicOpened((state.value as State.Mnemonic).mnemonicPhrase) } - viewModelScope.sendAnalyticsOnboardingClickEvent( - analytics = analytics, - type = EventsDictionary.ClickOnboardingButton.SHOW_AND_COPY, - step = EventsDictionary.ScreenOnboardingStep.PHRASE - ) + sendClickAnalytics(ClickOnboardingButton.SHOW_AND_COPY) } - fun onCheckLaterClicked( - space: Id, - startingObject: Id?, - ) { - viewModelScope.sendAnalyticsOnboardingClickEvent( - analytics = analytics, - type = EventsDictionary.ClickOnboardingButton.CHECK_LATER, - step = EventsDictionary.ScreenOnboardingStep.PHRASE - ) - if (shouldShowEmail()) { - viewModelScope.launch { - commands.emit( - Command.NavigateToAddEmailScreen( - startingObject = startingObject, - space = space - ) - ) - } - } else { - viewModelScope.launch { - val config = configStorage.getOrNull() - if (config != null) { - analytics.sendOpenAccountEvent( - analytics = config.analytics - ) - } else { - Timber.w("config was missing before the end of onboarding") - } - if (!startingObject.isNullOrEmpty()) { - commands.emit( - Command.OpenStartingObject( - space = SpaceId(space), - startingObject = startingObject - ) - ) - } else { - commands.emit(Command.OpenVault) - } - } + fun onCheckLaterClicked(space: Id, startingObject: Id?) { + sendClickAnalytics(ClickOnboardingButton.CHECK_LATER) + viewModelScope.launch { + navigateNextStep(space, startingObject) } } - fun onGoToTheAppClicked( - space: Id, - startingObject: Id?, - ) { + fun handleAppEntryClick(space: Id, startingObject: Id?) { + viewModelScope.launch { + navigateNextStep(space, startingObject) + } + } + + private suspend fun navigateNextStep(space: Id, startingObject: Id?) { if (shouldShowEmail()) { - viewModelScope.launch { - commands.emit( - Command.NavigateToAddEmailScreen( - startingObject = startingObject, - space = space - ) + emitNavigateToAddEmail(space, startingObject) + return + } + + logOpenAccountIfAvailable() + + val deeplink = pendingIntentStore.getDeepLinkInvite() + when { + !deeplink.isNullOrEmpty() -> emitCommand(Command.OpenVault) + !startingObject.isNullOrEmpty() -> emitCommand( + Command.OpenStartingObject( + space = SpaceId(space), + startingObject = startingObject ) - } + ) + + else -> emitCommand(Command.OpenVault) + } + } + + private suspend fun emitNavigateToAddEmail(space: Id, startingObject: Id?) { + emitCommand( + Command.NavigateToAddEmailScreen( + space = space, + startingObject = startingObject + ) + ) + } + + private suspend fun emitCommand(command: Command) { + commands.emit(command) + } + + private suspend fun logOpenAccountIfAvailable() { + val config = configStorage.getOrNull() + if (config != null) { + analytics.sendOpenAccountEvent(config.analytics) } else { - viewModelScope.launch { - val config = configStorage.getOrNull() - if (config != null) { - analytics.sendOpenAccountEvent( - analytics = config.analytics - ) - } else { - Timber.w("config was missing before the end of onboarding") - } - if (!startingObject.isNullOrEmpty()) { - commands.emit( - Command.OpenStartingObject( - space = SpaceId(space), - startingObject = startingObject - ) - ) - } else { - commands.emit(Command.OpenVault) - } - } + Timber.w("Missing config during onboarding") } } + private fun sendScreenViewAnalytics() { + viewModelScope.sendAnalyticsOnboardingScreenEvent( + analytics, + EventsDictionary.ScreenOnboardingStep.PHRASE + ) + } + fun shouldShowEmail(): Boolean { val networkStatus = networkConnectionStatus.getCurrentNetworkType() if (networkStatus == DeviceNetworkType.NOT_CONNECTED) { @@ -152,13 +134,21 @@ class OnboardingMnemonicViewModel @Inject constructor( ) } + private fun sendClickAnalytics(type: ClickOnboardingButton) { + viewModelScope.sendAnalyticsOnboardingClickEvent( + analytics = analytics, + type = type, + step = EventsDictionary.ScreenOnboardingStep.PHRASE + ) + } + sealed interface State { val mnemonicPhrase: String - class Idle(override val mnemonicPhrase: String): State - class Mnemonic(override val mnemonicPhrase: String): State - class MnemonicOpened(override val mnemonicPhrase: String): State + class Idle(override val mnemonicPhrase: String) : State + class Mnemonic(override val mnemonicPhrase: String) : State + class MnemonicOpened(override val mnemonicPhrase: String) : State } class Factory @Inject constructor( @@ -166,7 +156,8 @@ class OnboardingMnemonicViewModel @Inject constructor( private val analytics: Analytics, private val configStorage: ConfigStorage, private val networkModeProvider: NetworkModeProvider, - private val networkConnectionStatus: NetworkConnectionStatus + private val networkConnectionStatus: NetworkConnectionStatus, + private val pendingIntentStore: PendingIntentStore ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { @@ -175,7 +166,8 @@ class OnboardingMnemonicViewModel @Inject constructor( analytics = analytics, configStorage = configStorage, networkModeProvider = networkModeProvider, - networkConnectionStatus = networkConnectionStatus + networkConnectionStatus = networkConnectionStatus, + pendingIntentStore = pendingIntentStore ) as T } } @@ -186,6 +178,7 @@ class OnboardingMnemonicViewModel @Inject constructor( val space: SpaceId, val startingObject: Id ) : Command() + data class NavigateToAddEmailScreen( val startingObject: String?, val space: String diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/onboarding/signup/OnboardingSetProfileNameViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/onboarding/signup/OnboardingSetProfileNameViewModel.kt index db058b959e..2c139408d1 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/onboarding/signup/OnboardingSetProfileNameViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/onboarding/signup/OnboardingSetProfileNameViewModel.kt @@ -15,6 +15,7 @@ import com.anytypeio.anytype.domain.auth.interactor.CreateAccount import com.anytypeio.anytype.domain.auth.interactor.SetupWallet import com.anytypeio.anytype.domain.base.fold import com.anytypeio.anytype.domain.config.ConfigStorage +import com.anytypeio.anytype.domain.deeplink.PendingIntentStore import com.anytypeio.anytype.domain.device.PathProvider import com.anytypeio.anytype.domain.misc.LocaleProvider import com.anytypeio.anytype.domain.`object`.ImportGetStartedUseCase @@ -54,6 +55,7 @@ class OnboardingSetProfileNameViewModel @Inject constructor( private val spaceManager: SpaceManager, private val stringProvider: StringResourceProvider, private val setMembershipEmail: SetMembershipEmail, + private val pendingIntentStore: PendingIntentStore ) : BaseViewModel() { init { @@ -295,16 +297,26 @@ class OnboardingSetProfileNameViewModel @Inject constructor( private fun proceedWithNavigation(space: Id, startingObject: String?) { viewModelScope.launch { sendOpenAccountAnalytics() - if (!startingObject.isNullOrEmpty()) { - navigation.emit( - OpenStartingObject( - space = SpaceId(space), - startingObject = startingObject - ) + navigateNextStep( + space = space, + startingObject = startingObject + ) + } + } + + private suspend fun navigateNextStep(space: Id, startingObject: Id?) { + delay(LOADING_AFTER_SUCCESS_DELAY) + val deeplink = pendingIntentStore.getDeepLinkInvite() + when { + !deeplink.isNullOrEmpty() -> navigation.emit(Navigation.OpenVault) + !startingObject.isNullOrEmpty() -> navigation.emit( + OpenStartingObject( + space = SpaceId(space), + startingObject = startingObject ) - } else { - navigation.emit(Navigation.OpenVault) - } + ) + + else -> navigation.emit(Navigation.OpenVault) } } @@ -359,7 +371,8 @@ class OnboardingSetProfileNameViewModel @Inject constructor( private val globalSubscriptionManager: GlobalSubscriptionManager, private val spaceManager: SpaceManager, private val stringProvider: StringResourceProvider, - private val setMembershipEmail: SetMembershipEmail + private val setMembershipEmail: SetMembershipEmail, + private val pendingIntentStore: PendingIntentStore ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { @@ -378,7 +391,8 @@ class OnboardingSetProfileNameViewModel @Inject constructor( globalSubscriptionManager = globalSubscriptionManager, spaceManager = spaceManager, stringProvider = stringProvider, - setMembershipEmail = setMembershipEmail + setMembershipEmail = setMembershipEmail, + pendingIntentStore = pendingIntentStore ) as T } } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/filter/FilterViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/filter/FilterViewModel.kt index cb3781f27b..45e60cc479 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/filter/FilterViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/filter/FilterViewModel.kt @@ -460,13 +460,16 @@ open class FilterViewModel( } fun onConditionClicked() { - val condition = conditionState.value?.condition - checkNotNull(condition) viewModelScope.launch { - proceedWithConditionPickerScreen( - type = condition.type(), - index = condition.index() - ) + val condition = conditionState.value?.condition + if (condition != null) { + proceedWithConditionPickerScreen( + type = condition.type(), + index = condition.index() + ) + } else { + Timber.e("Unexpected state: condition was null in filter") + } } } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/util/StringResourceProviderImpl.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/util/StringResourceProviderImpl.kt index da1076bc65..a70800028f 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/util/StringResourceProviderImpl.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/util/StringResourceProviderImpl.kt @@ -55,4 +55,8 @@ class StringResourceProviderImpl @Inject constructor(private val context: Contex override fun getDefaultSpaceName(): String { return context.getString(R.string.onboarding_my_first_space) } + + override fun getAttachmentText(): String { + return context.getString(R.string.attachment) + } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/VaultViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/VaultViewModel.kt index 5bcfa9997f..26fddd5455 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/VaultViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/VaultViewModel.kt @@ -16,6 +16,7 @@ import com.anytypeio.anytype.core_models.primitives.Space import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.domain.base.fold import com.anytypeio.anytype.domain.chats.ChatPreviewContainer +import com.anytypeio.anytype.domain.deeplink.PendingIntentStore import com.anytypeio.anytype.domain.misc.AppActionManager import com.anytypeio.anytype.domain.misc.DeepLinkResolver import com.anytypeio.anytype.domain.misc.UrlBuilder @@ -48,7 +49,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map @@ -64,8 +64,6 @@ class VaultViewModel( private val getSpaceWallpapers: GetSpaceWallpapers, private val spaceManager: SpaceManager, private val saveCurrentSpace: SaveCurrentSpace, - private val getVaultSettings: GetVaultSettings, - private val setVaultSettings: SetVaultSettings, private val observeVaultSettings: ObserveVaultSettings, private val setVaultSpaceOrder: SetVaultSpaceOrder, private val analytics: Analytics, @@ -73,7 +71,8 @@ class VaultViewModel( private val appActionManager: AppActionManager, private val spaceInviteResolver: SpaceInviteResolver, private val profileContainer: ProfileSubscriptionManager, - private val chatPreviewContainer: ChatPreviewContainer + private val chatPreviewContainer: ChatPreviewContainer, + private val pendingIntentStore: PendingIntentStore ) : NavigationViewModel(), DeepLinkToObjectDelegate by deepLinkToObjectDelegate { val spaces = MutableStateFlow>(emptyList()) @@ -261,6 +260,17 @@ class VaultViewModel( } } + fun processPendingDeeplink() { + viewModelScope.launch { + delay(1000) // Simulate some delay + pendingIntentStore.getDeepLinkInvite()?.let { deeplink -> + Timber.d("Processing pending deeplink: $deeplink") + commands.emit(Command.Deeplink.Invite(deeplink)) + pendingIntentStore.clearDeepLinkInvite() + } + } + } + private suspend fun proceedWithSavingCurrentSpace( targetSpace: String, chat: Id?, @@ -357,8 +367,6 @@ class VaultViewModel( private val urlBuilder: UrlBuilder, private val spaceManager: SpaceManager, private val saveCurrentSpace: SaveCurrentSpace, - private val getVaultSettings: GetVaultSettings, - private val setVaultSettings: SetVaultSettings, private val setVaultSpaceOrder: SetVaultSpaceOrder, private val observeVaultSettings: ObserveVaultSettings, private val analytics: Analytics, @@ -366,7 +374,8 @@ class VaultViewModel( private val appActionManager: AppActionManager, private val spaceInviteResolver: SpaceInviteResolver, private val profileContainer: ProfileSubscriptionManager, - private val chatPreviewContainer: ChatPreviewContainer + private val chatPreviewContainer: ChatPreviewContainer, + private val pendingIntentStore: PendingIntentStore ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create( @@ -377,8 +386,6 @@ class VaultViewModel( urlBuilder = urlBuilder, spaceManager = spaceManager, saveCurrentSpace = saveCurrentSpace, - getVaultSettings = getVaultSettings, - setVaultSettings = setVaultSettings, setVaultSpaceOrder = setVaultSpaceOrder, observeVaultSettings = observeVaultSettings, analytics = analytics, @@ -386,7 +393,8 @@ class VaultViewModel( appActionManager = appActionManager, spaceInviteResolver = spaceInviteResolver, profileContainer = profileContainer, - chatPreviewContainer = chatPreviewContainer + chatPreviewContainer = chatPreviewContainer, + pendingIntentStore = pendingIntentStore ) as T } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/SpaceChatWidgetContainer.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/SpaceChatWidgetContainer.kt index eb24196218..a1d6f5328c 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/SpaceChatWidgetContainer.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/SpaceChatWidgetContainer.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart class SpaceChatWidgetContainer @Inject constructor( private val widget: Widget, @@ -32,6 +33,14 @@ class SpaceChatWidgetContainer @Inject constructor( unreadMentionCount = unreadMentionCount ) } + .onStart { + emit( + WidgetView.SpaceChat( + id = widget.id, + source = widget.source + ) + ) + } ) }.catch { emit( diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/Widget.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/Widget.kt index e968cd23c2..e056a46ede 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/Widget.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/Widget.kt @@ -10,6 +10,7 @@ import com.anytypeio.anytype.core_models.SupportedLayouts.isSupportedForWidgets import com.anytypeio.anytype.core_models.ext.asMap import com.anytypeio.anytype.core_models.widgets.BundledWidgetSourceIds import com.anytypeio.anytype.domain.primitives.FieldParser +import com.anytypeio.anytype.presentation.search.ObjectSearchConstants import com.anytypeio.anytype.presentation.widgets.WidgetView.Name sealed class Widget { @@ -122,6 +123,10 @@ sealed class Widget { override val type: Id? = null } } + + companion object { + val SOURCE_KEYS = ObjectSearchConstants.defaultKeys + } } } diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModelTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModelTest.kt index 214cd21ab6..93ffaddcc5 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModelTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModelTest.kt @@ -37,6 +37,7 @@ import com.anytypeio.anytype.domain.base.Resultat 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.config.ConfigStorage import com.anytypeio.anytype.domain.config.Gateway @@ -284,6 +285,9 @@ class HomeScreenViewModelTest { @Mock lateinit var setObjectListIsFavorite: SetObjectListIsFavorite + @Mock + lateinit var chacPreviewContainer: ChatPreviewContainer + lateinit var userPermissionProvider: UserPermissionProvider private val objectPayloadDispatcher = Dispatcher.Default() @@ -2930,7 +2934,8 @@ class HomeScreenViewModelTest { spaceMembers = activeSpaceMemberSubscriptionContainer, spaceViewSubscriptionContainer = spaceViewSubscriptionContainer, deleteSpace = deleteSpace, - setAsFavourite = setObjectListIsFavorite + setAsFavourite = setObjectListIsFavorite, + chatPreviews = chacPreviewContainer ) companion object { diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/onboarding/signup/OnboardingMnemonicViewModelTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/onboarding/signup/OnboardingMnemonicViewModelTest.kt index 8318c4ca98..c4c9a3b6f9 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/onboarding/signup/OnboardingMnemonicViewModelTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/onboarding/signup/OnboardingMnemonicViewModelTest.kt @@ -6,6 +6,7 @@ import com.anytypeio.anytype.core_models.NetworkMode import com.anytypeio.anytype.core_models.NetworkModeConfig import com.anytypeio.anytype.domain.auth.interactor.GetMnemonic import com.anytypeio.anytype.domain.config.ConfigStorage +import com.anytypeio.anytype.domain.deeplink.PendingIntentStore import com.anytypeio.anytype.domain.device.NetworkConnectionStatus import com.anytypeio.anytype.domain.network.NetworkModeProvider import com.anytypeio.anytype.presentation.util.DefaultCoroutineTestRule @@ -40,8 +41,11 @@ class OnboardingMnemonicViewModelTest { @Mock private lateinit var networkModeProvider: NetworkModeProvider + lateinit var pendingIntentStore: PendingIntentStore + @Before fun setup() { + pendingIntentStore = PendingIntentStore() MockitoAnnotations.openMocks(this) } @@ -139,7 +143,8 @@ class OnboardingMnemonicViewModelTest { analytics = analytics, configStorage = configStorage, networkModeProvider = networkModeProvider, - networkConnectionStatus = networkConnectionStatus + networkConnectionStatus = networkConnectionStatus, + pendingIntentStore = pendingIntentStore ) } } \ No newline at end of file diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/splash/SplashViewModelTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/splash/SplashViewModelTest.kt index 229a9ba318..9121b595a1 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/splash/SplashViewModelTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/splash/SplashViewModelTest.kt @@ -6,6 +6,7 @@ import com.anytypeio.anytype.CrashReporter import com.anytypeio.anytype.analytics.base.Analytics import com.anytypeio.anytype.core_models.StubConfig import com.anytypeio.anytype.core_models.StubSpaceView +import com.anytypeio.anytype.core_models.multiplayer.SpaceUxType import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.core_models.restrictions.SpaceStatus import com.anytypeio.anytype.domain.auth.interactor.CheckAuthorizationStatus @@ -319,7 +320,8 @@ class SplashViewModelTest { targetSpaceId = space, spaceLocalStatus = SpaceStatus.OK, spaceAccountStatus = SpaceStatus.OK, - chatId = chatId + chatId = chatId, + spaceUxType = SpaceUxType.CHAT ) stubCheckAuthStatus(response) @@ -357,6 +359,58 @@ class SplashViewModelTest { } } + @Test + fun `should navigate to vault even if chat is available if given space has data ux type`() = runTest { + // GIVEN + val deeplink = "test-deeplink" + val status = AuthStatus.AUTHORIZED + val response = Either.Right(status) + + val space = defaultSpaceConfig.space + val chatId = "chat-id" + + val spaceView = StubSpaceView( + targetSpaceId = space, + spaceLocalStatus = SpaceStatus.OK, + spaceAccountStatus = SpaceStatus.OK, + chatId = chatId, + spaceUxType = SpaceUxType.DATA + ) + + stubCheckAuthStatus(response) + stubLaunchWallet() + stubLaunchAccount() + stubGetLastOpenedObject() + + getLastOpenedSpace.stub { + onBlocking { async(Unit) } doReturn Resultat.Success(SpaceId(space)) + } + + initViewModel() + + // WHEN + spaceManager.stub { + on { observe() } doReturn flowOf(defaultSpaceConfig) + } + spaceViewSubscriptionContainer.stub { + on { observe(SpaceId(defaultSpaceConfig.space)) } doReturn flowOf(spaceView) + } + + vm.commands.test { + // Act + vm.onDeepLinkLaunch(deeplink) + + val first = awaitItem() + assertEquals( + expected = SplashViewModel.Command.NavigateToWidgets( + space = space, + deeplink = deeplink + ), + actual = first + ) + } + } + private fun stubCheckAuthStatus(response: Either.Right) { checkAuthorizationStatus.stub { onBlocking { invoke(eq(Unit)) } doReturn response diff --git a/protocol/src/main/proto/commands.proto b/protocol/src/main/proto/commands.proto index d4852a15c8..1ed4a41ff7 100644 --- a/protocol/src/main/proto/commands.proto +++ b/protocol/src/main/proto/commands.proto @@ -8508,6 +8508,28 @@ message Rpc { } } } + + + message ReadAll { + message Request {} + + message Response { + Error error = 1; + + message Error { + Code code = 1; + string description = 2; + + enum Code { + NULL = 0; + UNKNOWN_ERROR = 1; + BAD_INPUT = 2; + // ... + } + } + } + } + } message PushNotification { message RegisterToken { diff --git a/protocol/src/main/proto/events.proto b/protocol/src/main/proto/events.proto index 31da241fbb..394f6c9d55 100644 --- a/protocol/src/main/proto/events.proto +++ b/protocol/src/main/proto/events.proto @@ -817,6 +817,7 @@ message Event { anytype.model.Block.Content.Dataview.View.Size cardSize = 5; // Gallery card size bool coverFit = 6; // Image fits container string groupRelationKey = 7; // Group view by this relationKey + string endRelationKey = 16; bool groupBackgroundColors = 8; // Enable backgrounds in groups int32 pageLimit = 9; // Limit of objects shown in widget string defaultTemplateId = 10; // Id of template object set default for the view diff --git a/protocol/src/main/proto/models.proto b/protocol/src/main/proto/models.proto index a5a4bf4ca4..bdf88f5f3f 100644 --- a/protocol/src/main/proto/models.proto +++ b/protocol/src/main/proto/models.proto @@ -363,6 +363,7 @@ message Block { int32 pageLimit = 13; // Limit of objects shown in widget string defaultTemplateId = 14; // Default template that is chosen for new object created within the view string defaultObjectTypeId = 15; // Default object type that is chosen for new object created within the view + string endRelationKey = 16; // Group view by this relationKey enum Type { Table = 0; diff --git a/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/DataView.kt b/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/DataView.kt index 4109d42b06..9732093357 100644 --- a/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/DataView.kt +++ b/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/DataView.kt @@ -1,6 +1,7 @@ package com.anytypeio.anytype.core_models import com.anytypeio.anytype.core_models.multiplayer.SpaceAccessType +import com.anytypeio.anytype.core_models.multiplayer.SpaceUxType import com.anytypeio.anytype.core_models.restrictions.SpaceStatus import com.anytypeio.anytype.test_utils.MockDataFactory @@ -104,8 +105,8 @@ fun StubSpaceView( sharedSpaceLimit: Int? = null, spaceAccountStatus: SpaceStatus? = null, spaceLocalStatus: SpaceStatus? = null, - chatId: Id? = null - + chatId: Id? = null, + spaceUxType: SpaceUxType = SpaceUxType.DATA, ) = ObjectWrapper.SpaceView( map = mapOf( Relations.ID to id, @@ -114,6 +115,7 @@ fun StubSpaceView( Relations.SPACE_ACCESS_TYPE to spaceAccessType.code.toDouble(), Relations.SHARED_SPACES_LIMIT to sharedSpaceLimit?.toDouble(), Relations.SPACE_ACCOUNT_STATUS to spaceAccountStatus?.code?.toDouble(), - Relations.SPACE_LOCAL_STATUS to spaceLocalStatus?.code?.toDouble() + Relations.SPACE_LOCAL_STATUS to spaceLocalStatus?.code?.toDouble(), + Relations.SPACE_UX_TYPE to spaceUxType.code.toDouble() ) ) \ No newline at end of file diff --git a/versioning.gradle b/versioning.gradle index 881bc75273..ec94bd3931 100644 --- a/versioning.gradle +++ b/versioning.gradle @@ -98,7 +98,7 @@ ext.getBuildVersionName = { def date = getCurrentDate() return "${versionMajor}.${versionMinor}.${versionPatch}-${date}" } else { - return "${versionMajor}.${versionMinor}.${versionPatch}-beta" + return "${versionMajor}.${versionMinor}.${versionPatch}" } }