android, desktop: notes to self (#3695)

* android, desktop: notes to self

* change api

* icon

* icon

* icon

* eol

* icon

* changes

* color

* refactor

* color

* chats size

* size

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
This commit is contained in:
Stanislav Dmitrenko 2024-01-19 00:56:42 +07:00 committed by GitHub
parent c4d75366b5
commit ec57529f12
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 290 additions and 56 deletions

View File

@ -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 {

View File

@ -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) }

View File

@ -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

View File

@ -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"

View File

@ -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
}
}

View File

@ -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)
)

View File

@ -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,
)
}

View File

@ -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<M
fun itemInfoShareText(chatModel: ChatModel, ci: ChatItem, chatItemInfo: ChatItemInfo, devTools: Boolean): String {
val meta = ci.meta
val sent = ci.chatDir.sent
val shareText = mutableListOf<String>("# " + generalGetString(if (sent) MR.strings.sent_message else MR.strings.received_message), "")
val shareText = mutableListOf<String>("# " + 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)))
}

View File

@ -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,12 +624,28 @@ fun ChatInfoToolbar(
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
val menuItems = arrayListOf<@Composable () -> Unit>()
val activeCall by remember { chatModel.activeCall }
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)) {
if (activeCall == null) {
@ -743,16 +759,18 @@ fun ChatInfoToolbar(
}
}
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) {

View File

@ -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)

View File

@ -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) }

View File

@ -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))

View File

@ -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 -> {} // ?

View File

@ -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)

View File

@ -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)

View File

@ -95,6 +95,25 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
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<Boolean>, 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<Boolean>) {
ItemAction(
@ -347,12 +380,25 @@ fun ToggleNotificationsChatAction(chat: Chat, chatModel: ChatModel, ntfsEnabled:
}
@Composable
fun ClearChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
fun ClearChatAction(chat: Chat, showMenu: MutableState<Boolean>) {
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<Boolean>) {
ItemAction(
stringResource(MR.strings.clear_chat_menu_action),
painterResource(MR.images.ic_settings_backup_restore),
onClick = {
clearNoteFolderDialog(chat)
showMenu.value = false
},
color = WarningOrange

View File

@ -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

View File

@ -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)
) {
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

View File

@ -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

View File

@ -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
}
}

View File

@ -51,6 +51,9 @@
<string name="decryption_error">Decryption error</string>
<string name="encryption_renegotiation_error">Encryption re-negotiation error</string>
<!-- NoteFolder - ChatModel.kt -->
<string name="note_folder_local_display_name">Private notes</string>
<!-- PendingContactConnection - ChatModel.kt -->
<string name="connection_local_display_name">connection %1$d</string>
<string name="display_name_connection_established">connection established</string>
@ -99,6 +102,7 @@
<string name="connection_error">Connection error</string>
<string name="network_error_desc">Please check your network connection with %1$s and try again.</string>
<string name="error_sending_message">Error sending message</string>
<string name="error_creating_message">Error creating message</string>
<string name="error_loading_details">Error loading details</string>
<string name="error_adding_members">Error adding member(s)</string>
<string name="error_joining_group">Error joining group</string>
@ -116,6 +120,7 @@
<string name="sender_may_have_deleted_the_connection_request">Sender may have deleted the connection request.</string>
<string name="error_deleting_contact">Error deleting contact</string>
<string name="error_deleting_group">Error deleting group</string>
<string name="error_deleting_note_folder">Error deleting private notes</string>
<string name="error_deleting_contact_request">Error deleting contact request</string>
<string name="error_deleting_pending_contact_connection">Error deleting pending contact connection</string>
<string name="error_changing_address">Error changing address</string>
@ -471,7 +476,9 @@
<!-- Clear Chat - ChatListNavLinkView.kt -->
<string name="clear_chat_question">Clear chat?</string>
<string name="clear_note_folder_question">Clear private notes?</string>
<string name="clear_chat_warning">All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</string>
<string name="clear_note_folder_warning">All messages will be deleted - this cannot be undone!</string>
<string name="clear_verb">Clear</string>
<string name="clear_chat_button">Clear chat</string>
<string name="clear_chat_menu_action">Clear</string>
@ -1301,6 +1308,7 @@
<string name="info_row_database_id">Database ID</string>
<string name="info_row_updated_at">Record updated at</string>
<string name="info_row_sent_at">Sent at</string>
<string name="info_row_created_at">Created at</string>
<string name="info_row_received_at">Received at</string>
<string name="info_row_deleted_at">Deleted at</string>
<string name="info_row_moderated_at">Moderated at</string>
@ -1308,6 +1316,7 @@
<string name="share_text_database_id">Database ID: %d</string>
<string name="share_text_updated_at">Record updated at: %s</string>
<string name="share_text_sent_at">Sent at: %s</string>
<string name="share_text_created_at">Created at: %s</string>
<string name="share_text_received_at">Received at: %s</string>
<string name="share_text_deleted_at">Deleted at: %s</string>
<string name="share_text_moderated_at">Moderated at: %s</string>
@ -1317,6 +1326,7 @@
<string name="current_version_timestamp">%s (current)</string>
<string name="item_info_no_text">no text</string>
<string name="recipient_colon_delivery_status">%s: %s</string>
<string name="saved_message_title">Saved message</string>
<!-- GroupMemberInfoView.kt -->
<string name="button_remove_member_question">Remove member?</string>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24"
viewBox="0 -960 960 960"
width="24"
xmlns="http://www.w3.org/2000/svg">
<path
id="path151"
style="display:inline;fill:#ffffff;stroke-width:58"
d="M 480,-880 A 400,400 0 0 0 80,-480 400,400 0 0 0 480,-80 400,400 0 0 0 880,-480 400,400 0 0 0 480,-880 Z m -194.65461,218.29769 h 162.0477 l 33.18257,33.18257 h 194.07895 c 8.44741,0 16.07203,3.40719 22.90296,10.23849 6.83129,6.83091 10.2796,14.49667 10.2796,22.94408 v 263.85691 c 0,8.44742 -3.44831,16.07205 -10.2796,22.90295 -6.83093,6.83132 -14.45555,10.27962 -22.90296,10.27962 H 285.34539 c -8.83193,0 -16.59324,-3.4483 -23.23191,-10.27962 -6.63903,-6.8309 -9.95065,-14.45553 -9.95065,-22.90295 v -297.03948 c 0,-8.44742 3.31162,-16.07205 9.95065,-22.90295 6.63867,-6.83132 14.39998,-10.27962 23.23191,-10.27962 z" />
<rect
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:19.9532;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;stop-color:#000000;stop-opacity:1"
id="rect151"
width="500"
height="30"
x="220"
y="-540" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB