diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index c3a8ec280..45e0332da 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -186,7 +186,7 @@ struct ContentView: View { .onAppear { requestNtfAuthorization() // Local Authentication notice is to be shown on next start after onboarding is complete - if (!prefLANoticeShown && prefShowLANotice && !chatModel.chats.isEmpty) { + if (!prefLANoticeShown && prefShowLANotice && chatModel.chats.count > 2) { prefLANoticeShown = true alertManager.showAlert(laNoticeAlert()) } else if !chatModel.showCallView && CallController.shared.activeCallInvitation == nil { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index a233ba6d5..bfff3bf9f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -66,7 +66,7 @@ fun MainScreen() { !chatModel.controller.appPrefs.laNoticeShown.get() && showAdvertiseLAAlert && chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete - && chatModel.chats.count() > 1 + && chatModel.chats.size > 2 && chatModel.activeCallInvitation.value == null ) { AppLock.showLANotice(ChatModel.controller.appPrefs.laNoticeShown) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index e53459015..be0e6ce72 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -663,6 +663,7 @@ data class ShowingInvitation( enum class ChatType(val type: String) { Direct("@"), Group("#"), + Local("*"), ContactRequest("<@"), ContactConnection(":"); } @@ -782,6 +783,7 @@ data class Chat( get() = when (chatInfo) { is ChatInfo.Direct -> true is ChatInfo.Group -> chatInfo.groupInfo.membership.memberRole >= GroupMemberRole.Member + is ChatInfo.Local -> true else -> false } @@ -864,6 +866,30 @@ sealed class ChatInfo: SomeChat, NamedChat { } } + @Serializable @SerialName("local") + data class Local(val noteFolder: NoteFolder): ChatInfo() { + override val chatType get() = ChatType.Local + override val localDisplayName get() = noteFolder.localDisplayName + override val id get() = noteFolder.id + override val apiId get() = noteFolder.apiId + override val ready get() = noteFolder.ready + override val sendMsgEnabled get() = noteFolder.sendMsgEnabled + override val ntfsEnabled get() = noteFolder.ntfsEnabled + override val incognito get() = noteFolder.incognito + override fun featureEnabled(feature: ChatFeature) = noteFolder.featureEnabled(feature) + override val timedMessagesTTL: Int? get() = noteFolder.timedMessagesTTL + override val createdAt get() = noteFolder.createdAt + override val updatedAt get() = noteFolder.updatedAt + override val displayName get() = noteFolder.displayName + override val fullName get() = noteFolder.fullName + override val image get() = noteFolder.image + override val localAlias get() = noteFolder.localAlias + + companion object { + val sampleData = Local(NoteFolder.sampleData) + } + } + @Serializable @SerialName("contactRequest") class ContactRequest(val contactRequest: UserContactRequest): ChatInfo() { override val chatType get() = ChatType.ContactRequest @@ -1466,6 +1492,40 @@ class MemberSubError ( val memberError: ChatError ) +@Serializable +class NoteFolder( + val noteFolderId: Long, + val favorite: Boolean, + val unread: Boolean, + override val createdAt: Instant, + override val updatedAt: Instant +): SomeChat, NamedChat { + override val chatType get() = ChatType.Local + override val id get() = "*$noteFolderId" + override val apiId get() = noteFolderId + override val ready get() = true + override val sendMsgEnabled get() = true + override val ntfsEnabled get() = false + override val incognito get() = false + override fun featureEnabled(feature: ChatFeature) = feature == ChatFeature.Voice + override val timedMessagesTTL: Int? get() = null + override val displayName get() = generalGetString(MR.strings.note_folder_local_display_name) + override val fullName get() = "" + override val image get() = null + override val localAlias get() = "" + override val localDisplayName: String get() = "" + + companion object { + val sampleData = NoteFolder( + noteFolderId = 1, + favorite = false, + unread = false, + createdAt = Clock.System.now(), + updatedAt = Clock.System.now() + ) + } +} + @Serializable class UserContactRequest ( val contactRequestId: Long, @@ -1666,6 +1726,8 @@ data class ChatItem ( else -> null } + val localNote: Boolean = chatDir is CIDirection.LocalSnd || chatDir is CIDirection.LocalRcv + val isDeletedContent: Boolean get() = when (content) { is CIContent.SndDeleted -> true @@ -1933,12 +1995,16 @@ sealed class CIDirection { @Serializable @SerialName("directRcv") class DirectRcv: CIDirection() @Serializable @SerialName("groupSnd") class GroupSnd: CIDirection() @Serializable @SerialName("groupRcv") class GroupRcv(val groupMember: GroupMember): CIDirection() + @Serializable @SerialName("localSnd") class LocalSnd: CIDirection() + @Serializable @SerialName("localRcv") class LocalRcv: CIDirection() val sent: Boolean get() = when(this) { is DirectSnd -> true is DirectRcv -> false is GroupSnd -> true is GroupRcv -> false + is LocalSnd -> true + is LocalRcv -> false } } @@ -2254,6 +2320,8 @@ class CIQuote ( is CIDirection.DirectRcv -> null is CIDirection.GroupSnd -> membership?.displayName ?: generalGetString(MR.strings.sender_you_pronoun) is CIDirection.GroupRcv -> chatDir.groupMember.displayName + is CIDirection.LocalSnd -> generalGetString(MR.strings.sender_you_pronoun) + is CIDirection.LocalRcv -> null null -> null } @@ -2525,7 +2593,8 @@ private val rcvCancelAction: CancelAction = CancelAction( @Serializable enum class FileProtocol { @SerialName("smp") SMP, - @SerialName("xftp") XFTP; + @SerialName("xftp") XFTP, + @SerialName("local") LOCAL; } @Serializable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index a70b1190d..ab172e61f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -680,6 +680,17 @@ object ChatController { } } } + suspend fun apiCreateChatItem(rh: Long?, noteFolderId: Long, file: CryptoFile? = null, mc: MsgContent): AChatItem? { + val cmd = CC.ApiCreateChatItem(noteFolderId, file, mc) + val r = sendCmd(rh, cmd) + return when (r) { + is CR.NewChatItem -> r.chatItem + else -> { + apiErrorAlert("apiCreateChatItem", generalGetString(MR.strings.error_creating_message), r) + null + } + } + } suspend fun apiGetChatItemInfo(rh: Long?, type: ChatType, id: Long, itemId: Long): ChatItemInfo? { return when (val r = sendCmd(rh, CC.ApiGetChatItemInfo(type, id, itemId))) { @@ -990,6 +1001,7 @@ object ChatController { val titleId = when (type) { ChatType.Direct -> MR.strings.error_deleting_contact ChatType.Group -> MR.strings.error_deleting_group + ChatType.Local -> MR.strings.error_deleting_note_folder ChatType.ContactRequest -> MR.strings.error_deleting_contact_request ChatType.ContactConnection -> MR.strings.error_deleting_pending_contact_connection } @@ -1001,6 +1013,17 @@ object ChatController { return success } + fun clearChat(chat: Chat, close: (() -> Unit)? = null) { + withBGApi { + val updatedChatInfo = apiClearChat(chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId) + if (updatedChatInfo != null) { + chatModel.clearChat(chat.remoteHostId, updatedChatInfo) + ntfManager.cancelNotificationsForChat(chat.chatInfo.id) + close?.invoke() + } + } + } + suspend fun apiClearChat(rh: Long?, type: ChatType, id: Long): ChatInfo? { val r = sendCmd(rh, CC.ApiClearChat(type, id)) if (r is CR.ChatCleared) return r.chatInfo @@ -2243,6 +2266,7 @@ sealed class CC { class ApiGetChat(val type: ChatType, val id: Long, val pagination: ChatPagination, val search: String = ""): CC() class ApiGetChatItemInfo(val type: ChatType, val id: Long, val itemId: Long): CC() class ApiSendMessage(val type: ChatType, val id: Long, val file: CryptoFile?, val quotedItemId: Long?, val mc: MsgContent, val live: Boolean, val ttl: Int?): CC() + class ApiCreateChatItem(val noteFolderId: Long, val file: CryptoFile?, val mc: MsgContent): CC() class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent, val live: Boolean): CC() class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemId: Long, val mode: CIDeleteMode): CC() class ApiDeleteMemberChatItem(val groupId: Long, val groupMemberId: Long, val itemId: Long): CC() @@ -2374,6 +2398,9 @@ sealed class CC { val ttlStr = if (ttl != null) "$ttl" else "default" "/_send ${chatRef(type, id)} live=${onOff(live)} ttl=${ttlStr} json ${json.encodeToString(ComposedMessage(file, quotedItemId, mc))}" } + is ApiCreateChatItem -> { + "/_create *$noteFolderId json ${json.encodeToString(ComposedMessage(file, null, mc))}" + } is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId live=${onOff(live)} ${mc.cmdString}" is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} $itemId ${mode.deleteMode}" is ApiDeleteMemberChatItem -> "/_delete member item #$groupId $groupMemberId $itemId" @@ -2502,6 +2529,7 @@ sealed class CC { is ApiGetChat -> "apiGetChat" is ApiGetChatItemInfo -> "apiGetChatItemInfo" is ApiSendMessage -> "apiSendMessage" + is ApiCreateChatItem -> "apiCreateChatItem" is ApiUpdateChatItem -> "apiUpdateChatItem" is ApiDeleteChatItem -> "apiDeleteChatItem" is ApiDeleteMemberChatItem -> "apiDeleteMemberChatItem" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Color.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Color.kt index dc9ea2def..5eeedbb2a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Color.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Color.kt @@ -1,8 +1,12 @@ package chat.simplex.common.ui.theme import androidx.compose.material.LocalContentColor +import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.graphics.* +import chat.simplex.common.views.helpers.mixWith +import kotlin.math.min val Purple200 = Color(0xFFBB86FC) val Purple500 = Color(0xFF6200EE) @@ -27,5 +31,17 @@ val WarningOrange = Color(255, 127, 0, 255) val WarningYellow = Color(255, 192, 0, 255) val FileLight = Color(183, 190, 199, 255) val FileDark = Color(101, 101, 106, 255) +val SentMessageColor = Color(0x1E45B8FF) val MenuTextColor: Color @Composable get () = if (isInDarkTheme()) LocalContentColor.current.copy(alpha = 0.8f) else Color.Black +val NoteFolderIconColor: Color @Composable get() = with(CurrentColors.collectAsState().value.appColors.sentMessage) { + // Default color looks too light and better to have it here a little bit brighter + if (alpha == SentMessageColor.alpha) { + copy(min(SentMessageColor.alpha + 0.1f, 1f)) + } else { + // Color is non-standard and theme maker can choose color without alpha at all since the theme bound to dark/light variant, + // and it shouldn't be universal + this + } +} + diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt index bef0d7e34..5cf05f64c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt @@ -212,7 +212,7 @@ val DarkColorPalette = darkColors( ) val DarkColorPaletteApp = AppColors( title = SimplexBlue, - sentMessage = Color(0x1E45B8FF), + sentMessage = SentMessageColor, receivedMessage = Color(0x20B1B0B5) ) @@ -231,7 +231,7 @@ val LightColorPalette = lightColors( ) val LightColorPaletteApp = AppColors( title = SimplexBlue, - sentMessage = Color(0x1E45B8FF), + sentMessage = SentMessageColor, receivedMessage = Color(0x20B1B0B5) ) @@ -251,7 +251,7 @@ val SimplexColorPalette = darkColors( ) val SimplexColorPaletteApp = AppColors( title = Color(0xFF267BE5), - sentMessage = Color(0x1E45B8FF), + sentMessage = SentMessageColor, receivedMessage = Color(0x20B1B0B5) ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index 32f6d6a6d..f195c723f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* @@ -91,7 +92,7 @@ fun ChatInfoView( } }, deleteContact = { deleteContactDialog(chat, chatModel, close) }, - clearChat = { clearChatDialog(chat, chatModel, close) }, + clearChat = { clearChatDialog(chat, close) }, switchContactAddress = { showSwitchAddressAlert(switchAddress = { withBGApi { @@ -254,23 +255,22 @@ fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, notify } } -fun clearChatDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = null) { - val chatInfo = chat.chatInfo +fun clearChatDialog(chat: Chat, close: (() -> Unit)? = null) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.clear_chat_question), text = generalGetString(MR.strings.clear_chat_warning), confirmText = generalGetString(MR.strings.clear_verb), - onConfirm = { - withBGApi { - val chatRh = chat.remoteHostId - val updatedChatInfo = chatModel.controller.apiClearChat(chatRh, chatInfo.chatType, chatInfo.apiId) - if (updatedChatInfo != null) { - chatModel.clearChat(chatRh, updatedChatInfo) - ntfManager.cancelNotificationsForChat(chatInfo.id) - close?.invoke() - } - } - }, + onConfirm = { controller.clearChat(chat, close) }, + destructive = true, + ) +} + +fun clearNoteFolderDialog(chat: Chat, close: (() -> Unit)? = null) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.clear_note_folder_question), + text = generalGetString(MR.strings.clear_note_folder_warning), + confirmText = generalGetString(MR.strings.clear_verb), + onConfirm = { controller.clearChat(chat, close) }, destructive = true, ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt index 3754315d0..a46821452 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt @@ -154,9 +154,9 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d @Composable fun Details() { - AppBarTitle(stringResource(if (sent) MR.strings.sent_message else MR.strings.received_message)) + AppBarTitle(stringResource(if (ci.localNote) MR.strings.saved_message_title else if (sent) MR.strings.sent_message else MR.strings.received_message)) SectionView { - InfoRow(stringResource(MR.strings.info_row_sent_at), localTimestamp(ci.meta.itemTs)) + InfoRow(stringResource(if (!ci.localNote) MR.strings.info_row_sent_at else MR.strings.info_row_created_at), localTimestamp(ci.meta.itemTs)) if (!sent) { InfoRow(stringResource(MR.strings.info_row_received_at), localTimestamp(ci.meta.createdAt)) } @@ -393,9 +393,9 @@ private fun membersStatuses(chatModel: ChatModel, memberDeliveryStatuses: List("# " + generalGetString(if (sent) MR.strings.sent_message else MR.strings.received_message), "") + val shareText = mutableListOf("# " + generalGetString(if (ci.localNote) MR.strings.saved_message_title else if (sent) MR.strings.sent_message else MR.strings.received_message), "") - shareText.add(String.format(generalGetString(MR.strings.share_text_sent_at), localTimestamp(meta.itemTs))) + shareText.add(String.format(generalGetString(if (ci.localNote) MR.strings.share_text_created_at else MR.strings.share_text_sent_at), localTimestamp(meta.itemTs))) if (!ci.chatDir.sent) { shareText.add(String.format(generalGetString(MR.strings.share_text_received_at), localTimestamp(meta.createdAt))) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 8a6c74bb3..2e99d791b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -117,7 +117,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: } val clipboard = LocalClipboardManager.current when (chat.chatInfo) { - is ChatInfo.Direct, is ChatInfo.Group -> { + is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> { ChatLayout( chat, unreadCount, @@ -624,11 +624,27 @@ fun ChatInfoToolbar( val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() val menuItems = arrayListOf<@Composable () -> Unit>() val activeCall by remember { chatModel.activeCall } - menuItems.add { - ItemAction(stringResource(MR.strings.search_verb), painterResource(MR.images.ic_search), onClick = { - showMenu.value = false - showSearch = true - }) + if (chat.chatInfo is ChatInfo.Local) { + barButtons.add { + IconButton({ + showMenu.value = false + showSearch = true + }, enabled = chat.chatInfo.noteFolder.ready + ) { + Icon( + painterResource(MR.images.ic_search), + stringResource(MR.strings.search_verb).capitalize(Locale.current), + tint = if (chat.chatInfo.noteFolder.ready) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + ) + } + } + } else { + menuItems.add { + ItemAction(stringResource(MR.strings.search_verb), painterResource(MR.images.ic_search), onClick = { + showMenu.value = false + showSearch = true + }) + } } if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.allowsFeature(ChatFeature.Calls)) { @@ -743,16 +759,18 @@ fun ChatInfoToolbar( } } - barButtons.add { - IconButton({ showMenu.value = true }) { - Icon(MoreVertFilled, stringResource(MR.strings.icon_descr_more_button), tint = MaterialTheme.colors.primary) + if (menuItems.isNotEmpty()) { + barButtons.add { + IconButton({ showMenu.value = true }) { + Icon(MoreVertFilled, stringResource(MR.strings.icon_descr_more_button), tint = MaterialTheme.colors.primary) + } } } DefaultTopAppBar( navigationButton = { if (appPlatform.isAndroid || showSearch) { NavigationButtonBack(onBackClicked) } }, title = { ChatInfoToolbarTitle(chat.chatInfo) }, - onTitleClick = info, + onTitleClick = if (chat.chatInfo is ChatInfo.Local) null else info, showSearch = showSearch, onSearchValueChanged = onSearchValueChanged, buttons = barButtons @@ -910,7 +928,7 @@ fun BoxWithConstraintsScope.ChatItemsList( if (dismissState.isAnimationRunning && (swipedToStart || swipedToEnd)) { LaunchedEffect(Unit) { scope.launch { - if (cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) { + if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chat.chatInfo !is ChatInfo.Local) { if (composeState.value.editing) { composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) } else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index e46e7a305..cd0c424e0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -353,7 +353,10 @@ fun ComposeView( suspend fun send(chat: Chat, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?): ChatItem? { val cInfo = chat.chatInfo - val aChatItem = chatModel.controller.apiSendMessage( + val aChatItem = if (chat.chatInfo.chatType == ChatType.Local) + chatModel.controller.apiCreateChatItem(rh = chat.remoteHostId, noteFolderId = chat.chatInfo.apiId, file = file, mc = mc) + else + chatModel.controller.apiSendMessage( rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, @@ -877,7 +880,7 @@ fun ComposeView( sendMessage(ttl) resetLinkPreview() }, - sendLiveMessage = ::sendLiveMessage, + sendLiveMessage = if (chat.chatInfo.chatType != ChatType.Local) ::sendLiveMessage else null, updateLiveMessage = ::updateLiveMessage, cancelLiveMessage = { composeState.value = composeState.value.copy(liveMessage = null) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index e412dd025..4d76afcbb 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -110,7 +110,7 @@ fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLi } }, deleteGroup = { deleteGroupDialog(chat, groupInfo, chatModel, close) }, - clearChat = { clearChatDialog(chat, chatModel, close) }, + clearChat = { clearChatDialog(chat, close) }, leaveGroup = { leaveGroupDialog(rhId, groupInfo, chatModel, close) }, manageGroupLink = { ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt index 79af76da8..24e9fd691 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt @@ -68,8 +68,8 @@ fun CIFileView( fun fileAction() { if (file != null) { - when (file.fileStatus) { - is CIFileStatus.RcvInvitation -> { + when { + file.fileStatus is CIFileStatus.RcvInvitation -> { if (fileSizeValid()) { receiveFile(file.fileId) } else { @@ -79,7 +79,7 @@ fun CIFileView( ) } } - is CIFileStatus.RcvAccepted -> + file.fileStatus is CIFileStatus.RcvAccepted -> when (file.fileProtocol) { FileProtocol.XFTP -> AlertManager.shared.showAlertMsg( @@ -91,8 +91,9 @@ fun CIFileView( generalGetString(MR.strings.waiting_for_file), generalGetString(MR.strings.file_will_be_received_when_contact_is_online) ) + FileProtocol.LOCAL -> {} } - is CIFileStatus.RcvComplete -> { + file.fileStatus is CIFileStatus.RcvComplete || (file.fileStatus is CIFileStatus.SndStored && file.fileProtocol == FileProtocol.LOCAL) -> { withBGApi { var filePath = getLoadedFilePath(file) if (chatModel.connectedToRemote() && filePath == null) { @@ -152,11 +153,13 @@ fun CIFileView( when (file.fileProtocol) { FileProtocol.XFTP -> progressIndicator() FileProtocol.SMP -> fileIcon() + FileProtocol.LOCAL -> fileIcon() } is CIFileStatus.SndTransfer -> when (file.fileProtocol) { FileProtocol.XFTP -> progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal) FileProtocol.SMP -> progressIndicator() + FileProtocol.LOCAL -> {} } is CIFileStatus.SndComplete -> fileIcon(innerIcon = painterResource(MR.images.ic_check_filled)) is CIFileStatus.SndCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt index cbec5c289..5aed7742b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt @@ -70,6 +70,7 @@ fun CIImageView( when (file.fileProtocol) { FileProtocol.XFTP -> progressIndicator() FileProtocol.SMP -> {} + FileProtocol.LOCAL -> {} } is CIFileStatus.SndTransfer -> progressIndicator() is CIFileStatus.SndComplete -> fileIcon(painterResource(MR.images.ic_check_filled), MR.strings.icon_descr_image_snd_complete) @@ -199,6 +200,7 @@ fun CIImageView( generalGetString(MR.strings.waiting_for_image), generalGetString(MR.strings.image_will_be_received_when_contact_is_online) ) + FileProtocol.LOCAL -> {} } CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ? CIFileStatus.RcvComplete -> {} // ? diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt index 609a895e4..413d95b67 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt @@ -82,12 +82,12 @@ fun CIVideoView( generalGetString(MR.strings.waiting_for_video), generalGetString(MR.strings.video_will_be_received_when_contact_completes_uploading) ) - FileProtocol.SMP -> AlertManager.shared.showAlertMsg( generalGetString(MR.strings.waiting_for_video), generalGetString(MR.strings.video_will_be_received_when_contact_is_online) ) + FileProtocol.LOCAL -> {} } CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ? CIFileStatus.RcvComplete -> {} // ? @@ -377,11 +377,13 @@ private fun loadingIndicator(file: CIFile?) { when (file.fileProtocol) { FileProtocol.XFTP -> progressIndicator() FileProtocol.SMP -> {} + FileProtocol.LOCAL -> {} } is CIFileStatus.SndTransfer -> when (file.fileProtocol) { FileProtocol.XFTP -> progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal) FileProtocol.SMP -> progressIndicator() + FileProtocol.LOCAL -> {} } is CIFileStatus.SndComplete -> fileIcon(painterResource(MR.images.ic_check_filled), MR.strings.icon_descr_video_snd_complete) is CIFileStatus.SndCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 6a1aeb21c..073d10887 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -183,7 +183,7 @@ fun ChatItemView( if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) { MsgReactionsMenu() } - if (cItem.meta.itemDeleted == null && !live) { + if (cItem.meta.itemDeleted == null && !live && !cItem.localNote) { ItemAction(stringResource(MR.strings.reply_verb), painterResource(MR.images.ic_reply), onClick = { if (composeState.value.editing) { composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) @@ -240,7 +240,7 @@ fun ChatItemView( if (revealed.value) { HideItemAction(revealed, showMenu) } - if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null) { + if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null && !cItem.localNote) { CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction) } if (!(live && cItem.meta.isLive)) { @@ -677,7 +677,7 @@ fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMes deleteMessage(chatItem.id, CIDeleteMode.cidmInternal) AlertManager.shared.hideAlert() }) { Text(stringResource(MR.strings.for_me_only), color = MaterialTheme.colors.error) } - if (chatItem.meta.editable) { + if (chatItem.meta.editable && !chatItem.localNote) { Spacer(Modifier.padding(horizontal = 4.dp)) TextButton(onClick = { deleteMessage(chatItem.id, CIDeleteMode.cidmBroadcast) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index a80c70398..15b1db6c4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -95,6 +95,25 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { selectedChat, nextChatSelected, ) + is ChatInfo.Local -> { + ChatListNavLinkLayout( + chatLinkPreview = { + tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) { + ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, disabled, linkMode, inProgress = false, progressByTimeout = false) + } + }, + click = { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) }, + dropdownMenuItems = { + tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { + NoteFolderMenuItems(chat, showMenu, showMarkRead) + } + }, + showMenu, + disabled, + selectedChat, + nextChatSelected, + ) + } is ChatInfo.ContactRequest -> ChatListNavLinkLayout( chatLinkPreview = { @@ -174,6 +193,10 @@ fun groupChatAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, inP } } +fun noteFolderChatAction(rhId: Long?, noteFolder: NoteFolder) { + withBGApi { openChat(rhId, ChatInfo.Local(noteFolder), chatModel) } +} + suspend fun openDirectChat(rhId: Long?, contactId: Long, chatModel: ChatModel) { val chat = chatModel.controller.apiGetChat(rhId, ChatType.Direct, contactId) if (chat != null) { @@ -247,7 +270,7 @@ fun ContactMenuItems(chat: Chat, contact: Contact, chatModel: ChatModel, showMen } ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu) ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu) - ClearChatAction(chat, chatModel, showMenu) + ClearChatAction(chat, showMenu) } DeleteContactAction(chat, chatModel, showMenu) } @@ -286,7 +309,7 @@ fun GroupMenuItems( } ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu) ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu) - ClearChatAction(chat, chatModel, showMenu) + ClearChatAction(chat, showMenu) if (groupInfo.membership.memberCurrent) { LeaveGroupAction(chat.remoteHostId, groupInfo, chatModel, showMenu) } @@ -297,6 +320,16 @@ fun GroupMenuItems( } } +@Composable +fun NoteFolderMenuItems(chat: Chat, showMenu: MutableState, showMarkRead: Boolean) { + if (showMarkRead) { + MarkReadChatAction(chat, chatModel, showMenu) + } else { + MarkUnreadChatAction(chat, chatModel, showMenu) + } + ClearNoteFolderAction(chat, showMenu) +} + @Composable fun MarkReadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState) { ItemAction( @@ -347,12 +380,25 @@ fun ToggleNotificationsChatAction(chat: Chat, chatModel: ChatModel, ntfsEnabled: } @Composable -fun ClearChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState) { +fun ClearChatAction(chat: Chat, showMenu: MutableState) { ItemAction( stringResource(MR.strings.clear_chat_menu_action), painterResource(MR.images.ic_settings_backup_restore), onClick = { - clearChatDialog(chat, chatModel) + clearChatDialog(chat) + showMenu.value = false + }, + color = WarningOrange + ) +} + +@Composable +fun ClearNoteFolderAction(chat: Chat, showMenu: MutableState) { + ItemAction( + stringResource(MR.strings.clear_chat_menu_action), + painterResource(MR.images.ic_settings_backup_restore), + onClick = { + clearNoteFolderDialog(chat) showMenu.value = false }, color = WarningOrange diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 28d276b26..3a47d062a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -518,6 +518,7 @@ private fun filteredChats( } else { viewNameContains(cInfo, s) } + is ChatInfo.Local -> s.isEmpty() || viewNameContains(cInfo, s) is ChatInfo.ContactRequest -> s.isEmpty() || viewNameContains(cInfo, s) is ChatInfo.ContactConnection -> (s.isNotEmpty() && cInfo.contactConnection.localAlias.lowercase().contains(s)) || (s.isEmpty() && chat.id == chatModel.chatId.value) is ChatInfo.InvalidJSON -> chat.id == chatModel.chatId.value diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListNavLinkView.kt index ad8f93990..1de1e40af 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListNavLinkView.kt @@ -9,9 +9,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import chat.simplex.common.ui.theme.Indigo import chat.simplex.common.views.helpers.ProfileImage import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.res.MR @Composable fun ShareListNavLinkView(chat: Chat, chatModel: ChatModel) { @@ -29,6 +30,12 @@ fun ShareListNavLinkView(chat: Chat, chatModel: ChatModel) { click = { groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel) }, stopped ) + is ChatInfo.Local -> + ShareListNavLinkLayout( + chatLinkPreview = { SharePreviewView(chat) }, + click = { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) }, + stopped + ) is ChatInfo.ContactRequest, is ChatInfo.ContactConnection, is ChatInfo.InvalidJSON -> {} } } @@ -56,7 +63,11 @@ private fun SharePreviewView(chat: Chat) { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - ProfileImage(size = 46.dp, chat.chatInfo.image) + if (chat.chatInfo is ChatInfo.Local) { + ProfileImage(size = 46.dp, null, icon = MR.images.ic_folder_filled, color = NoteFolderIconColor) + } else { + ProfileImage(size = 46.dp, chat.chatInfo.image) + } Text( chat.chatInfo.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis, color = if (chat.chatInfo.incognito) Indigo else Color.Unspecified diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt index abc894942..c667ed9ac 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import chat.simplex.common.model.ChatInfo import chat.simplex.common.platform.base64ToBitmap +import chat.simplex.common.ui.theme.NoteFolderIconColor import chat.simplex.common.ui.theme.SimpleXTheme import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource @@ -24,9 +25,12 @@ import dev.icerock.moko.resources.ImageResource @Composable fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme.colors.secondaryVariant) { val icon = - if (chatInfo is ChatInfo.Group) MR.images.ic_supervised_user_circle_filled - else MR.images.ic_account_circle_filled - ProfileImage(size, chatInfo.image, icon, iconColor) + when (chatInfo) { + is ChatInfo.Group -> MR.images.ic_supervised_user_circle_filled + is ChatInfo.Direct -> MR.images.ic_account_circle_filled + else -> MR.images.ic_folder_filled + } + ProfileImage(size, chatInfo.image, icon, if (chatInfo is ChatInfo.Local) NoteFolderIconColor else iconColor) } @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index a067cb2dd..75c730b72 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -132,6 +132,8 @@ const val MAX_FILE_SIZE_SMP: Long = 8000000 const val MAX_FILE_SIZE_XFTP: Long = 1_073_741_824 // 1GB +const val MAX_FILE_SIZE_LOCAL: Long = Long.MAX_VALUE + expect fun getAppFileUri(fileName: String): URI // https://developer.android.com/training/data-storage/shared/documents-files#bitmap @@ -357,6 +359,7 @@ fun getMaxFileSize(fileProtocol: FileProtocol): Long { return when (fileProtocol) { FileProtocol.XFTP -> MAX_FILE_SIZE_XFTP FileProtocol.SMP -> MAX_FILE_SIZE_SMP + FileProtocol.LOCAL -> MAX_FILE_SIZE_LOCAL } } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index e3711afaf..47cf4bf3a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -51,6 +51,9 @@ Decryption error Encryption re-negotiation error + + Private notes + connection %1$d connection established @@ -99,6 +102,7 @@ Connection error Please check your network connection with %1$s and try again. Error sending message + Error creating message Error loading details Error adding member(s) Error joining group @@ -116,6 +120,7 @@ Sender may have deleted the connection request. Error deleting contact Error deleting group + Error deleting private notes Error deleting contact request Error deleting pending contact connection Error changing address @@ -471,7 +476,9 @@ Clear chat? + Clear private notes? All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you. + All messages will be deleted - this cannot be undone! Clear Clear chat Clear @@ -1301,6 +1308,7 @@ Database ID Record updated at Sent at + Created at Received at Deleted at Moderated at @@ -1308,6 +1316,7 @@ Database ID: %d Record updated at: %s Sent at: %s + Created at: %s Received at: %s Deleted at: %s Moderated at: %s @@ -1317,6 +1326,7 @@ %s (current) no text %s: %s + Saved message Remove member? diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_filled.svg new file mode 100644 index 000000000..f4c284a5f --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_filled.svg @@ -0,0 +1,18 @@ + + + + +