mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-18 18:16:53 +00:00
Merge branch 'master' into ep/improve-names-2
This commit is contained in:
+23
-1
@@ -2261,6 +2261,7 @@ data class GroupInfo (
|
||||
GroupFeature.Reports -> p.reports.on
|
||||
GroupFeature.History -> p.history.on
|
||||
GroupFeature.Support -> p.support.on
|
||||
GroupFeature.SignMessages -> p.signMessages.on
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2422,6 +2423,26 @@ data class GroupShortLinkData (
|
||||
val publicGroupData: PublicGroupData? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class MsgSigStatus {
|
||||
@SerialName("verified") Verified,
|
||||
@SerialName("signedNoKey") SignedNoKey;
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class MsgVerified {
|
||||
@Serializable @SerialName("signed") data class Signed(val sigStatus: MsgSigStatus): MsgVerified()
|
||||
@Serializable @SerialName("sigMissing") object SigMissing: MsgVerified()
|
||||
@Serializable @SerialName("unsigned") object Unsigned: MsgVerified()
|
||||
|
||||
val verified: Boolean get() = this is Signed && sigStatus == MsgSigStatus.Verified
|
||||
|
||||
val sigMissingInfo: Pair<String, String>? get() = when (this) {
|
||||
is SigMissing -> generalGetString(MR.strings.signature_missing_alert_title) to generalGetString(MR.strings.signature_missing_alert_desc)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class RelayStatus {
|
||||
@SerialName("new") New,
|
||||
@@ -3555,7 +3576,8 @@ data class CIMeta (
|
||||
val userMention: Boolean,
|
||||
val deletable: Boolean,
|
||||
val editable: Boolean,
|
||||
val showGroupAsSender: Boolean
|
||||
val showGroupAsSender: Boolean,
|
||||
val msgVerified: MsgVerified = MsgVerified.Unsigned
|
||||
) {
|
||||
val timestampText: String get() = getTimestampText(itemTs, true)
|
||||
|
||||
|
||||
+29
-5
@@ -129,6 +129,8 @@ class AppPreferences {
|
||||
val privacyChatListOpenLinks = mkEnumPreference(SHARED_PREFS_PRIVACY_CHAT_LIST_OPEN_LINKS, PrivacyChatListOpenLinksMode.ASK) { PrivacyChatListOpenLinksMode.values().firstOrNull { it.name == this } }
|
||||
val simplexLinkMode: SharedPreference<SimplexLinkMode> = mkSafeEnumPreference(SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE, SimplexLinkMode.default)
|
||||
val privacyShowChatPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS, true)
|
||||
val privacyShowSignature = mkBoolPreference(SHARED_PREFS_PRIVACY_SHOW_SIGNATURE, true)
|
||||
val privacyShowEncryption = mkBoolPreference(SHARED_PREFS_PRIVACY_SHOW_FILE_ENCRYPTION, true)
|
||||
val privacySaveLastDraft = mkBoolPreference(SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT, true)
|
||||
val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false)
|
||||
val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true)
|
||||
@@ -186,6 +188,7 @@ class AppPreferences {
|
||||
val networkTCPKeepCnt = mkIntPreference(SHARED_PREFS_NETWORK_TCP_KEEP_CNT, KeepAliveOpts.defaults.keepCnt)
|
||||
val incognito = mkBoolPreference(SHARED_PREFS_INCOGNITO, false)
|
||||
val liveMessageAlertShown = mkBoolPreference(SHARED_PREFS_LIVE_MESSAGE_ALERT_SHOWN, false)
|
||||
val signMessageAlertShown = mkBoolPreference(SHARED_PREFS_SIGN_MESSAGE_ALERT_SHOWN, false)
|
||||
val showHiddenProfilesNotice = mkBoolPreference(SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE, true)
|
||||
val oneHandUICardShown = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN, false)
|
||||
val addressCreationCardShown = mkBoolPreference(SHARED_PREFS_ADDRESS_CREATION_CARD_SHOWN, false)
|
||||
@@ -271,6 +274,7 @@ class AppPreferences {
|
||||
hintPref(oneHandUICardShown, false),
|
||||
hintPref(addressCreationCardShown, false),
|
||||
hintPref(liveMessageAlertShown, false),
|
||||
hintPref(signMessageAlertShown, false),
|
||||
hintPref(showHiddenProfilesNotice, true),
|
||||
hintPref(showMuteProfileAlert, true),
|
||||
hintPref(showReportsInSupportChatAlert, true),
|
||||
@@ -404,6 +408,8 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_PRIVACY_CHAT_LIST_OPEN_LINKS = "ChatListOpenLinks" // TODO remove
|
||||
private const val SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE = "PrivacySimplexLinkMode"
|
||||
private const val SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS = "PrivacyShowChatPreviews"
|
||||
private const val SHARED_PREFS_PRIVACY_SHOW_SIGNATURE = "PrivacyShowSignature"
|
||||
private const val SHARED_PREFS_PRIVACY_SHOW_FILE_ENCRYPTION = "PrivacyShowEncryption"
|
||||
private const val SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT = "PrivacySaveLastDraft"
|
||||
private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet"
|
||||
private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles"
|
||||
@@ -454,6 +460,7 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_NETWORK_TCP_KEEP_CNT = "NetworkTCPKeepCnt"
|
||||
private const val SHARED_PREFS_INCOGNITO = "Incognito"
|
||||
private const val SHARED_PREFS_LIVE_MESSAGE_ALERT_SHOWN = "LiveMessageAlertShown"
|
||||
private const val SHARED_PREFS_SIGN_MESSAGE_ALERT_SHOWN = "SignMessageAlertShown"
|
||||
private const val SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE = "ShowHiddenProfilesNotice"
|
||||
private const val SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN = "OneHandUICardShown"
|
||||
private const val SHARED_PREFS_ADDRESS_CREATION_CARD_SHOWN = "AddressCreationCardShown"
|
||||
@@ -1096,8 +1103,8 @@ object ChatController {
|
||||
|
||||
suspend fun apiReorderChatTags(rh: Long?, tagIds: List<Long>) = sendCommandOkResp(rh, CC.ApiReorderChatTags(tagIds))
|
||||
|
||||
suspend fun apiSendMessages(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, sendAsGroup: Boolean = false, live: Boolean = false, ttl: Int? = null, composedMessages: List<ComposedMessage>): List<AChatItem>? {
|
||||
val cmd = CC.ApiSendMessages(type, id, scope, sendAsGroup, live, ttl, composedMessages)
|
||||
suspend fun apiSendMessages(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, sendAsGroup: Boolean = false, live: Boolean = false, ttl: Int? = null, sign: Boolean = false, composedMessages: List<ComposedMessage>): List<AChatItem>? {
|
||||
val cmd = CC.ApiSendMessages(type, id, scope, sendAsGroup, live, ttl, sign, composedMessages)
|
||||
return processSendMessageCmd(rh, cmd)
|
||||
}
|
||||
|
||||
@@ -3784,7 +3791,7 @@ sealed class CC {
|
||||
class ApiGetChat(val type: ChatType, val id: Long, val scope: GroupChatScope?, val contentTag: MsgContentTag?, val pagination: ChatPagination, val search: String = ""): CC()
|
||||
class ApiGetChatContentTypes(val type: ChatType, val id: Long, val scope: GroupChatScope?): CC()
|
||||
class ApiGetChatItemInfo(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemId: Long): CC()
|
||||
class ApiSendMessages(val type: ChatType, val id: Long, val scope: GroupChatScope?, val sendAsGroup: Boolean, val live: Boolean, val ttl: Int?, val composedMessages: List<ComposedMessage>): CC()
|
||||
class ApiSendMessages(val type: ChatType, val id: Long, val scope: GroupChatScope?, val sendAsGroup: Boolean, val live: Boolean, val ttl: Int?, val sign: Boolean, val composedMessages: List<ComposedMessage>): CC()
|
||||
class ApiCreateChatTag(val tag: ChatTagData): CC()
|
||||
class ApiSetChatTags(val type: ChatType, val id: Long, val tagIds: List<Long>): CC()
|
||||
class ApiDeleteChatTag(val tagId: Long): CC()
|
||||
@@ -3982,7 +3989,7 @@ sealed class CC {
|
||||
is ApiSendMessages -> {
|
||||
val msgs = json.encodeToString(composedMessages)
|
||||
val ttlStr = if (ttl != null) "$ttl" else "default"
|
||||
"/_send ${chatRef(type, id, scope)}${if (sendAsGroup) "(as_group=on)" else ""} live=${onOff(live)} ttl=${ttlStr} json $msgs"
|
||||
"/_send ${chatRef(type, id, scope)}${if (sendAsGroup) "(as_group=on)" else ""} live=${onOff(live)} ttl=${ttlStr} sign=${onOff(sign)} json $msgs"
|
||||
}
|
||||
is ApiCreateChatTag -> "/_create tag ${json.encodeToString(tag)}"
|
||||
is ApiSetChatTags -> "/_tags ${chatRef(type, id, scope = null)} ${tagIds.joinToString(",")}"
|
||||
@@ -5857,7 +5864,8 @@ enum class GroupFeature: Feature {
|
||||
@SerialName("simplexLinks") SimplexLinks,
|
||||
@SerialName("reports") Reports,
|
||||
@SerialName("history") History,
|
||||
@SerialName("support") Support;
|
||||
@SerialName("support") Support,
|
||||
@SerialName("signMessages") SignMessages;
|
||||
|
||||
override val hasParam: Boolean get() = when(this) {
|
||||
TimedMessages -> true
|
||||
@@ -5876,6 +5884,7 @@ enum class GroupFeature: Feature {
|
||||
Reports -> false
|
||||
History -> false
|
||||
Support -> false
|
||||
SignMessages -> false
|
||||
}
|
||||
|
||||
override val text: String get() = text(isChannel = false)
|
||||
@@ -5891,6 +5900,7 @@ enum class GroupFeature: Feature {
|
||||
Reports -> generalGetString(if (isChannel) MR.strings.group_reports_subscriber_reports else MR.strings.group_reports_member_reports)
|
||||
History -> generalGetString(MR.strings.recent_history)
|
||||
Support -> generalGetString(MR.strings.chat_with_admins)
|
||||
SignMessages -> generalGetString(MR.strings.sign_messages)
|
||||
}
|
||||
|
||||
val icon: Painter
|
||||
@@ -5905,6 +5915,7 @@ enum class GroupFeature: Feature {
|
||||
Reports -> painterResource(MR.images.ic_flag)
|
||||
History -> painterResource(MR.images.ic_schedule)
|
||||
Support -> painterResource(MR.images.ic_help)
|
||||
SignMessages -> painterResource(MR.images.ic_verified)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -5919,6 +5930,7 @@ enum class GroupFeature: Feature {
|
||||
Reports -> painterResource(MR.images.ic_flag_filled)
|
||||
History -> painterResource(MR.images.ic_schedule_filled)
|
||||
Support -> painterResource(MR.images.ic_help_filled)
|
||||
SignMessages -> painterResource(MR.images.ic_verified_filled)
|
||||
}
|
||||
|
||||
fun enableDescription(enabled: GroupFeatureEnabled, canEdit: Boolean, isChannel: Boolean = false): String =
|
||||
@@ -5964,6 +5976,10 @@ enum class GroupFeature: Feature {
|
||||
GroupFeatureEnabled.ON -> generalGetString(if (isChannel) MR.strings.allow_chat_with_admins_channel else MR.strings.allow_chat_with_admins)
|
||||
GroupFeatureEnabled.OFF -> generalGetString(MR.strings.prohibit_chat_with_admins)
|
||||
}
|
||||
SignMessages -> when(enabled) {
|
||||
GroupFeatureEnabled.ON -> generalGetString(MR.strings.require_message_signatures)
|
||||
GroupFeatureEnabled.OFF -> generalGetString(MR.strings.do_not_require_message_signatures)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
when(this) {
|
||||
@@ -6007,6 +6023,10 @@ enum class GroupFeature: Feature {
|
||||
GroupFeatureEnabled.ON -> generalGetString(if (isChannel) MR.strings.members_can_chat_with_admins_channel else MR.strings.members_can_chat_with_admins)
|
||||
GroupFeatureEnabled.OFF -> generalGetString(MR.strings.chat_with_admins_is_prohibited)
|
||||
}
|
||||
SignMessages -> when(enabled) {
|
||||
GroupFeatureEnabled.ON -> generalGetString(MR.strings.message_signatures_are_required)
|
||||
GroupFeatureEnabled.OFF -> generalGetString(MR.strings.message_signatures_are_not_required)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6133,6 +6153,7 @@ data class FullGroupPreferences(
|
||||
val reports: GroupPreference,
|
||||
val history: GroupPreference,
|
||||
val support: GroupPreference,
|
||||
val signMessages: GroupPreference,
|
||||
val commands: List<ChatBotCommand>,
|
||||
) {
|
||||
fun toGroupPreferences(): GroupPreferences =
|
||||
@@ -6147,6 +6168,7 @@ data class FullGroupPreferences(
|
||||
reports = reports,
|
||||
history = history,
|
||||
support = support,
|
||||
signMessages = signMessages,
|
||||
commands = commands,
|
||||
)
|
||||
|
||||
@@ -6162,6 +6184,7 @@ data class FullGroupPreferences(
|
||||
reports = GroupPreference(GroupFeatureEnabled.ON),
|
||||
history = GroupPreference(GroupFeatureEnabled.ON),
|
||||
support = GroupPreference(GroupFeatureEnabled.ON),
|
||||
signMessages = GroupPreference(GroupFeatureEnabled.OFF),
|
||||
commands = listOf()
|
||||
)
|
||||
}
|
||||
@@ -6179,6 +6202,7 @@ data class GroupPreferences(
|
||||
val reports: GroupPreference? = null,
|
||||
val history: GroupPreference? = null,
|
||||
val support: GroupPreference? = null,
|
||||
val signMessages: GroupPreference? = null,
|
||||
val commands: List<ChatBotCommand>? = null
|
||||
) {
|
||||
companion object {
|
||||
|
||||
+2
@@ -392,6 +392,8 @@ fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: ()
|
||||
null, Profile(displayName.trim(), "", null, null)
|
||||
) ?: return@withBGApi
|
||||
chatModel.localUserCreated.value = true
|
||||
// new users don't need the local file encryption indicator (all files are encrypted); existing users keep it on
|
||||
chatModel.controller.appPrefs.privacyShowEncryption.set(false)
|
||||
val onboardingStage = chatModel.controller.appPrefs.onboardingStage
|
||||
// No users or no visible users
|
||||
if (chatModel.users.none { u -> !u.user.hidden }) {
|
||||
|
||||
+15
-1
@@ -272,7 +272,16 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
if (deleteAt != null) {
|
||||
InfoRow(stringResource(MR.strings.info_row_disappears_at), localTimestamp(deleteAt))
|
||||
}
|
||||
if (devTools) {
|
||||
if (ci.meta.msgVerified.verified) {
|
||||
val signedRes = if (sent) MR.strings.info_row_signed else MR.strings.info_row_signed_verified
|
||||
InfoRow(stringResource(signedRes), "", icon = painterResource(MR.images.ic_verified))
|
||||
} else if (ci.meta.msgVerified is MsgVerified.SigMissing) {
|
||||
InfoRow(stringResource(MR.strings.signature_missing_alert_title), "", icon = painterResource(MR.images.ic_verified_missing), iconTint = Color.Red)
|
||||
}
|
||||
}
|
||||
if (devTools) {
|
||||
SectionDividerSpaced()
|
||||
SectionView {
|
||||
InfoRow(stringResource(MR.strings.info_row_database_id), ci.meta.itemId.toString())
|
||||
InfoRow(stringResource(MR.strings.info_row_updated_at), localTimestamp(ci.meta.updatedAt))
|
||||
ExpandableInfoRow(stringResource(MR.strings.info_row_message_status), jsonShort.encodeToString(ci.meta.itemStatus))
|
||||
@@ -559,6 +568,11 @@ fun itemInfoShareText(chatModel: ChatModel, ci: ChatItem, chatItemInfo: ChatItem
|
||||
if (deleteAt != null) {
|
||||
shareText.add(String.format(generalGetString(MR.strings.share_text_disappears_at), localTimestamp(deleteAt)))
|
||||
}
|
||||
if (ci.meta.msgVerified.verified) {
|
||||
shareText.add(generalGetString(if (sent) MR.strings.info_row_signed else MR.strings.info_row_signed_verified))
|
||||
} else if (ci.meta.msgVerified is MsgVerified.SigMissing) {
|
||||
shareText.add(generalGetString(MR.strings.signature_missing_alert_title))
|
||||
}
|
||||
if (devTools) {
|
||||
shareText.add(String.format(generalGetString(MR.strings.share_text_database_id), meta.itemId))
|
||||
shareText.add(String.format(generalGetString(MR.strings.share_text_updated_at), meta.updatedAt))
|
||||
|
||||
+16
-8
@@ -539,7 +539,7 @@ fun ComposeView(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun send(chat: Chat, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?, mentions: Map<String, Long>): ChatItem? {
|
||||
suspend fun send(chat: Chat, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?, mentions: Map<String, Long>, sign: Boolean = false): ChatItem? {
|
||||
val cInfo = chat.chatInfo
|
||||
val chatItems = if (chat.chatInfo.chatType == ChatType.Local)
|
||||
chatModel.controller.apiCreateChatItems(
|
||||
@@ -556,6 +556,7 @@ fun ComposeView(
|
||||
sendAsGroup = cInfo.sendAsGroup,
|
||||
live = live,
|
||||
ttl = ttl,
|
||||
sign = sign,
|
||||
composedMessages = listOf(ComposedMessage(file, quoted, mc, mentions))
|
||||
)
|
||||
if (!chatItems.isNullOrEmpty()) {
|
||||
@@ -672,7 +673,7 @@ fun ComposeView(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): List<ChatItem>? {
|
||||
suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?, sign: Boolean = false): List<ChatItem>? {
|
||||
val cs = composeState.value
|
||||
var sent: List<ChatItem>?
|
||||
var lastMessageFailedToSend: ComposeState? = null
|
||||
@@ -797,7 +798,7 @@ fun ComposeView(
|
||||
if (cs.message.text.isNotEmpty()) {
|
||||
sent?.mapIndexed { index, message ->
|
||||
if (index == sent!!.lastIndex) {
|
||||
send(chat, checkLinkPreview(), quoted = message.id, live = false, ttl = ttl, mentions = cs.memberMentions)
|
||||
send(chat, checkLinkPreview(), quoted = message.id, live = false, ttl = ttl, mentions = cs.memberMentions, sign = sign)
|
||||
} else {
|
||||
message
|
||||
}
|
||||
@@ -837,7 +838,7 @@ fun ComposeView(
|
||||
if (remoteHost == null) saveAnimImage(it.uri)
|
||||
else CryptoFile.desktopPlain(it.uri)
|
||||
is UploadContent.Video ->
|
||||
if (remoteHost == null) saveFileFromUri(it.uri, hiddenFileNamePrefix = "video")
|
||||
if (remoteHost == null) saveFileFromUri(it.uri, cs.maxFileSize, hiddenFileNamePrefix = "video")
|
||||
else CryptoFile.desktopPlain(it.uri)
|
||||
}
|
||||
if (file != null) {
|
||||
@@ -886,7 +887,7 @@ fun ComposeView(
|
||||
}
|
||||
is ComposePreview.FilePreview -> {
|
||||
val file = if (remoteHost == null) {
|
||||
saveFileFromUri(preview.uri)
|
||||
saveFileFromUri(preview.uri, cs.maxFileSize)
|
||||
} else {
|
||||
CryptoFile.desktopPlain(preview.uri)
|
||||
}
|
||||
@@ -914,7 +915,8 @@ fun ComposeView(
|
||||
val sendResult = send(chat, content, if (index == 0) quotedItemId else null, file,
|
||||
live = if (content !is MsgContent.MCVoice && index == msgs.lastIndex) live else false,
|
||||
ttl = ttl,
|
||||
mentions = cs.memberMentions
|
||||
mentions = cs.memberMentions,
|
||||
sign = sign
|
||||
)
|
||||
sent = if (sendResult != null) listOf(sendResult) else null
|
||||
if (sent == null && index == msgs.lastIndex && cs.liveMessage == null) {
|
||||
@@ -941,9 +943,9 @@ fun ComposeView(
|
||||
return sent
|
||||
}
|
||||
|
||||
fun sendMessage(ttl: Int?) {
|
||||
fun sendMessage(ttl: Int?, sign: Boolean = false) {
|
||||
withLongRunningApi(slow = 120_000) {
|
||||
sendMessageAsync(null, false, ttl)
|
||||
sendMessageAsync(null, false, ttl, sign)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1387,12 +1389,18 @@ fun ComposeView(
|
||||
allowVoiceToContact = ::allowVoiceToContact,
|
||||
sendButtonColor = sendButtonColor,
|
||||
timedMessageAllowed = timedMessageAllowed,
|
||||
showSign = (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.useRelays == true,
|
||||
signMessageAlertShown = chatModel.controller.appPrefs.signMessageAlertShown,
|
||||
customDisappearingMessageTimePref = chatModel.controller.appPrefs.customDisappearingMessageTime,
|
||||
placeholder = if (userCantSendReason.value != null) "" else placeholder ?: composeState.value.placeholder,
|
||||
sendMessage = { ttl ->
|
||||
sendMessage(ttl)
|
||||
resetLinkPreview()
|
||||
},
|
||||
sendSignedMessage = {
|
||||
sendMessage(null, sign = true)
|
||||
resetLinkPreview()
|
||||
},
|
||||
sendLiveMessage = if (chat.chatInfo.chatType != ChatType.Local) ::sendLiveMessage else null,
|
||||
updateLiveMessage = ::updateLiveMessage,
|
||||
cancelLiveMessage = {
|
||||
|
||||
+27
@@ -49,9 +49,12 @@ fun SendMsgView(
|
||||
sendButtonColor: Color = MaterialTheme.colors.primary,
|
||||
allowVoiceToContact: () -> Unit,
|
||||
timedMessageAllowed: Boolean = false,
|
||||
showSign: Boolean = false,
|
||||
signMessageAlertShown: SharedPreference<Boolean> = SharedPreference(get = { false }, set = {}),
|
||||
customDisappearingMessageTimePref: SharedPreference<Int>? = null,
|
||||
placeholder: String,
|
||||
sendMessage: (Int?) -> Unit,
|
||||
sendSignedMessage: () -> Unit = {},
|
||||
sendLiveMessage: (suspend () -> Unit)? = null,
|
||||
updateLiveMessage: (suspend () -> Unit)? = null,
|
||||
cancelLiveMessage: (() -> Unit)? = null,
|
||||
@@ -207,6 +210,30 @@ fun SendMsgView(
|
||||
)
|
||||
}
|
||||
}
|
||||
if (showSign && !cs.editing) {
|
||||
menuItems.add {
|
||||
ItemAction(
|
||||
generalGetString(MR.strings.sign_message),
|
||||
painterResource(MR.images.ic_verified),
|
||||
onClick = {
|
||||
if (signMessageAlertShown.state.value) {
|
||||
sendSignedMessage()
|
||||
} else {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.sign_message),
|
||||
text = generalGetString(MR.strings.sign_message_desc),
|
||||
confirmText = generalGetString(MR.strings.send_verb),
|
||||
onConfirm = {
|
||||
signMessageAlertShown.set(true)
|
||||
sendSignedMessage()
|
||||
}
|
||||
)
|
||||
}
|
||||
showDropdown.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return menuItems
|
||||
|
||||
+8
@@ -178,6 +178,12 @@ private fun GroupPreferencesLayout(
|
||||
}
|
||||
}
|
||||
}
|
||||
@Composable fun SignMessagesPreference() {
|
||||
val enableSignMessages = remember(preferences) { mutableStateOf(preferences.signMessages.enable) }
|
||||
FeatureSection(GroupFeature.SignMessages, enableSignMessages, null, groupInfo, preferences, onTTLUpdated) { enable, _ ->
|
||||
applyPrefs(preferences.copy(signMessages = GroupPreference(enable = enable)))
|
||||
}
|
||||
}
|
||||
ColumnWithScrollBar {
|
||||
val titleId = if (groupInfo.useRelays) MR.strings.channel_preferences
|
||||
else if (groupInfo.businessChat == null) MR.strings.group_preferences
|
||||
@@ -210,6 +216,8 @@ private fun GroupPreferencesLayout(
|
||||
SectionDividerSpaced()
|
||||
SupportPreference(disabled = true)
|
||||
} else {
|
||||
SignMessagesPreference()
|
||||
SectionDividerSpaced()
|
||||
TimedMessagesPreference()
|
||||
SectionDividerSpaced()
|
||||
FullDeletePreference()
|
||||
|
||||
+1
-1
@@ -211,7 +211,7 @@ fun CIFileView(
|
||||
val encrypted = if (file?.fileSource == null) null else file.fileSource.cryptoArgs != null
|
||||
val metaReserve = buildAnnotatedString {
|
||||
withStyle(reserveTimestampStyle) {
|
||||
append(reserveSpaceForMeta(meta, chatTTL, encrypted, secondaryColor = secondaryColor, showViaProxy = showViaProxy, showTimestamp = showTimestamp))
|
||||
append(reserveSpaceForMeta(meta, chatTTL, encrypted, secondaryColor = secondaryColor, showViaProxy = showViaProxy, showTimestamp = showTimestamp, signedFileVerified = file?.loaded))
|
||||
}
|
||||
}
|
||||
if (file != null) {
|
||||
|
||||
+24
-4
@@ -13,6 +13,7 @@ import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.appPreferences
|
||||
import chat.simplex.common.ui.theme.isInDarkTheme
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Clock
|
||||
@@ -56,7 +57,8 @@ fun CIMetaView(
|
||||
showStatus = showStatus,
|
||||
showEdited = showEdited,
|
||||
showViaProxy = showViaProxy,
|
||||
showTimestamp = showTimestamp
|
||||
showTimestamp = showTimestamp,
|
||||
signedFileVerified = chatItem.file?.loaded
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -74,7 +76,10 @@ private fun CIMetaText(
|
||||
showEdited: Boolean = true,
|
||||
showTimestamp: Boolean,
|
||||
showViaProxy: Boolean,
|
||||
signedFileVerified: Boolean?,
|
||||
) {
|
||||
val showSignature = appPreferences.privacyShowSignature.state.value
|
||||
val showEncryption = appPreferences.privacyShowEncryption.state.value
|
||||
if (showEdited && meta.itemEdited) {
|
||||
StatusIconText(painterResource(MR.images.ic_edit), color)
|
||||
}
|
||||
@@ -103,10 +108,17 @@ private fun CIMetaText(
|
||||
StatusIconText(painterResource(MR.images.ic_circle_filled), Color.Transparent)
|
||||
}
|
||||
}
|
||||
if (encrypted != null) {
|
||||
if (encrypted != null && showEncryption) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
StatusIconText(painterResource(if (encrypted) MR.images.ic_lock else MR.images.ic_lock_open_right), color)
|
||||
}
|
||||
if (showSignature && meta.msgVerified.verified && signedFileVerified != false) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
StatusIconText(painterResource(MR.images.ic_verified), color)
|
||||
} else if (meta.msgVerified is MsgVerified.SigMissing) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
StatusIconText(painterResource(MR.images.ic_verified_missing), Color.Red)
|
||||
}
|
||||
|
||||
if (showTimestamp) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
@@ -123,8 +135,11 @@ fun reserveSpaceForMeta(
|
||||
showStatus: Boolean = true,
|
||||
showEdited: Boolean = true,
|
||||
showViaProxy: Boolean = false,
|
||||
showTimestamp: Boolean
|
||||
showTimestamp: Boolean,
|
||||
signedFileVerified: Boolean? = null
|
||||
): String {
|
||||
val showSignature = appPreferences.privacyShowSignature.state.value
|
||||
val showEncryption = appPreferences.privacyShowEncryption.state.value
|
||||
val iconSpace = " \u00A0\u00A0\u00A0"
|
||||
val whiteSpace = "\u00A0"
|
||||
var res = if (showTimestamp) "" else iconSpace
|
||||
@@ -162,7 +177,12 @@ fun reserveSpaceForMeta(
|
||||
space = whiteSpace
|
||||
}
|
||||
|
||||
if (encrypted != null) {
|
||||
if (encrypted != null && showEncryption) {
|
||||
appendSpace()
|
||||
res += iconSpace
|
||||
space = whiteSpace
|
||||
}
|
||||
if ((showSignature && meta.msgVerified.verified && signedFileVerified != false) || meta.msgVerified is MsgVerified.SigMissing) {
|
||||
appendSpace()
|
||||
res += iconSpace
|
||||
space = whiteSpace
|
||||
|
||||
+1
-1
@@ -127,7 +127,7 @@ fun ChatItemView(
|
||||
modifier = (if (fillMaxWidth) Modifier.fillMaxWidth() else Modifier),
|
||||
contentAlignment = alignment,
|
||||
) {
|
||||
val info = cItem.meta.itemStatus.statusInto
|
||||
val info = cItem.meta.itemStatus.statusInto ?: cItem.meta.msgVerified.sigMissingInfo
|
||||
val onClick = if (info != null) {
|
||||
{
|
||||
AlertManager.shared.showAlertMsg(
|
||||
|
||||
+33
-2
@@ -256,6 +256,7 @@ expect suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean)
|
||||
|
||||
fun saveFileFromUri(
|
||||
uri: URI,
|
||||
maxBytes: Long,
|
||||
withAlertOnException: Boolean = true,
|
||||
hiddenFileNamePrefix: String? = null
|
||||
): CryptoFile? {
|
||||
@@ -277,7 +278,7 @@ fun saveFileFromUri(
|
||||
val destFile = File(getAppFilePath(destFileName))
|
||||
if (encrypted) {
|
||||
createTmpFileAndDelete { tmpFile ->
|
||||
Files.copy(inputStream, tmpFile.toPath())
|
||||
copyInputStreamToFile(inputStream, tmpFile, maxBytes)
|
||||
try {
|
||||
val args = encryptCryptoFile(tmpFile.absolutePath, destFile.absolutePath)
|
||||
CryptoFile(destFileName, args)
|
||||
@@ -288,7 +289,7 @@ fun saveFileFromUri(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Files.copy(inputStream, destFile.toPath())
|
||||
copyInputStreamToFile(inputStream, destFile, maxBytes)
|
||||
CryptoFile.plain(destFileName)
|
||||
}
|
||||
} else {
|
||||
@@ -297,6 +298,15 @@ fun saveFileFromUri(
|
||||
|
||||
null
|
||||
}
|
||||
} catch (e: FileTooLargeException) {
|
||||
Log.e(TAG, "Util.kt saveFileFromUri file too large: ${e.message}")
|
||||
if (withAlertOnException) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxBytes))
|
||||
)
|
||||
}
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Util.kt saveFileFromUri error: ${e.stackTraceToString()}")
|
||||
if (withAlertOnException) showWrongUriAlert()
|
||||
@@ -305,6 +315,27 @@ fun saveFileFromUri(
|
||||
}
|
||||
}
|
||||
|
||||
class FileTooLargeException(maxBytes: Long) : IOException("file exceeds $maxBytes bytes")
|
||||
|
||||
fun copyInputStreamToFile(inputStream: InputStream, destFile: File, maxBytes: Long) {
|
||||
try {
|
||||
destFile.outputStream().use { output ->
|
||||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||
var copied = 0L
|
||||
while (true) {
|
||||
val read = inputStream.read(buffer)
|
||||
if (read < 0) break
|
||||
if (copied > maxBytes - read) throw FileTooLargeException(maxBytes)
|
||||
output.write(buffer, 0, read)
|
||||
copied += read
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
destFile.delete()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
fun saveWallpaperFile(uri: URI): String? {
|
||||
val destFileName = generateNewFileName("wallpaper", "jpg", File(getWallpaperFilePath("")))
|
||||
val destFile = File(getWallpaperFilePath(destFileName))
|
||||
|
||||
+6
@@ -139,6 +139,11 @@ fun MorePrivacyView(chatModel: ChatModel) {
|
||||
stringResource(MR.strings.verify_simplex_names),
|
||||
chatModel.controller.appPrefs.privacyVerifySimplexNames
|
||||
)
|
||||
SettingsPreferenceItem(
|
||||
painterResource(MR.images.ic_verified),
|
||||
stringResource(MR.strings.show_signature),
|
||||
chatModel.controller.appPrefs.privacyShowSignature
|
||||
)
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
@@ -147,6 +152,7 @@ fun MorePrivacyView(chatModel: ChatModel) {
|
||||
withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) }
|
||||
})
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_security), stringResource(MR.strings.protect_ip_address), chatModel.controller.appPrefs.privacyAskToApproveRelays)
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.show_encryption), chatModel.controller.appPrefs.privacyShowEncryption)
|
||||
}
|
||||
SectionTextFooter(
|
||||
if (chatModel.controller.appPrefs.privacyAskToApproveRelays.state.value) {
|
||||
|
||||
@@ -759,6 +759,19 @@
|
||||
<string name="send_disappearing_message_5_minutes">5 minutes</string>
|
||||
<string name="send_disappearing_message_custom_time">Custom time</string>
|
||||
<string name="send_disappearing_message_send">Send</string>
|
||||
<string name="sign_message">Sign message</string>
|
||||
<string name="sign_message_desc">Signing proves you authored this message and can\'t be denied later.</string>
|
||||
<string name="info_row_signed">Signed</string>
|
||||
<string name="info_row_signed_verified">Signed & verified</string>
|
||||
<string name="sign_messages">Sign messages</string>
|
||||
<string name="require_message_signatures">Require signing messages.</string>
|
||||
<string name="do_not_require_message_signatures">Do not require signing messages.</string>
|
||||
<string name="message_signatures_are_required">Message signing is required.</string>
|
||||
<string name="message_signatures_are_not_required">Message signing is not required.</string>
|
||||
<string name="show_signature">Show signature</string>
|
||||
<string name="show_encryption">Show encryption</string>
|
||||
<string name="signature_missing_alert_title">Signature missing</string>
|
||||
<string name="signature_missing_alert_desc">The channel required this message to be signed, but the signature is missing.</string>
|
||||
<string name="live_message">Live message!</string>
|
||||
<string name="send_live_message_desc">Send a live message - it will update for the recipient(s) as you type it</string>
|
||||
<string name="send_verb">Send</string>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="m437.5-432-73-76.5q-9-9.5-21.5-9.75T321-510q-10 10-10 22.5t10 22l96.5 95.5q8.5 9 20.05 9 11.54 0 20.45-9l181.4-180.4q8.6-8.6 8.6-21.1 0-12.5-9.5-21.5-9-7.5-20.75-7T598-591.5L437.5-432Zm-103 343L273-194l-123.5-25q-10.48-2.07-17.74-11.36-7.26-9.29-5.26-20.14l14-119.14-78-91.36q-7.5-7.5-7.5-19.05t7.5-19.45l78-90.15-14-118.85q-2-10.88 5.25-20.19t17.75-11.81L273-765l61.5-106.5q6-9.5 16.5-13.5t21 1l108 51 108-51q10.33-4.5 20.92-1.25Q619.5-882 625.5-872.5L688-765l122.5 24.5q10.5 2.5 17.75 11.81t5.25 20.19l-14 118.85L898-499.5q7 7.9 7 19.45 0 11.55-7.14 19.15l-78.36 91.32 14 119.08q2 10.85-5.26 20.14-7.26 9.29-17.74 11.36L688-194 625.5-88q-6 10-16.58 13.25Q598.33-71.5 588-76l-108-51-108 51q-10.33 4.5-20.92.75Q340.5-79 334.5-89Zm39.5-52.5L480-186l109 44.5 67-99 115.95-30.18-12.09-117.75L840.5-480l-80.64-93.58 12.09-117.76L656-719.5l-69-99L480-774l-109-44.5-67 99-115.95 28.16 12.09 117.76L119.5-480l80.64 91.57-12.09 119.77L304-240.5l70 99ZM480-480Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="m437.5-432-73-76.5q-9-9.5-21.5-9.75T321-510q-10 10-10 22.5t10 22l96.5 95.5q8.5 9 20 9t20.5-9l181.5-180.5q8.5-8.5 8.5-21t-9.5-21.5q-9-7.5-20.75-7T598-591.5L437.5-432Zm-103 343L273-194l-123.5-25q-10.5-2-17.75-11.25t-5.25-20.25l14-119-78-91.5q-7.5-7.5-7.5-19t7.5-19.5l78-90-14-119q-2-11 5.25-20.25t17.75-11.75L273-765l61.5-106.5q6-9.5 16.5-13.5t21 1l108 51 108-51q10.5-4.5 21-1.25t16.5 12.75L688-765l122.5 24.5q10.5 2.5 17.75 11.75t5.25 20.25l-14 119 78.5 90q7 8 7 19.5t-7 19l-78.5 91.5 14 119q2 11-5.25 20.25T810.5-219L688-194 625.5-88q-6 10-16.5 13.25T588-76l-108-51-108 51q-10.5 4.5-21 .75T334.5-89Z"/></svg>
|
||||
|
After Width: | Height: | Size: 724 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M334.5-89L273-194l-123.5-25q-10.48-2.07-17.74-11.36-7.26-9.29-5.26-20.14l14-119.14-78-91.36q-7.5-7.5-7.5-19.05t7.5-19.45l78-90.15-14-118.85q-2-10.88 5.25-20.19t17.75-11.81L273-765l61.5-106.5q6-9.5 16.5-13.5t21 1l108 51 108-51q10.33-4.5 20.92-1.25Q619.5-882 625.5-872.5L688-765l122.5 24.5q10.5 2.5 17.75 11.81t5.25 20.19l-14 118.85L898-499.5q7 7.9 7 19.45 0 11.55-7.14 19.15l-78.36 91.32 14 119.08q2 10.85-5.26 20.14-7.26 9.29-17.74 11.36L688-194 625.5-88q-6 10-16.58 13.25Q598.33-71.5 588-76l-108-51-108 51q-10.33 4.5-20.92.75Q340.5-79 334.5-89Zm39.5-52.5L480-186l109 44.5 67-99 115.95-30.18-12.09-117.75L840.5-480l-80.64-93.58 12.09-117.76L656-719.5l-69-99L480-774l-109-44.5-67 99-115.95 28.16 12.09 117.76L119.5-480l80.64 91.57-12.09 119.77L304-240.5l70 99Z"/><path d="M557.782 -600.208L359.792 -402.218C348.076 -390.503 348.076 -371.508 359.792 -359.792C371.508 -348.076 390.503 -348.076 402.218 -359.792L600.208 -557.782C611.924 -569.497 611.924 -588.492 600.208 -600.208C588.492 -611.924 569.497 -611.924 557.782 -600.208ZM359.792 -557.782L557.782 -359.792C569.497 -348.076 588.492 -348.076 600.208 -359.792C611.924 -371.508 611.924 -390.503 600.208 -402.218L402.218 -600.208C390.503 -611.924 371.508 -611.924 359.792 -600.208C348.076 -588.492 348.076 -569.497 359.792 -557.782Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
+70
@@ -0,0 +1,70 @@
|
||||
package chat.simplex.app
|
||||
|
||||
import chat.simplex.common.views.helpers.FileTooLargeException
|
||||
import chat.simplex.common.views.helpers.copyInputStreamToFile
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import kotlin.io.path.createTempFile
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class UtilsFileCopyTest {
|
||||
@Test
|
||||
fun testCopyInputStreamAllowsLimitBoundary() {
|
||||
val dest = createTempFile().toFile()
|
||||
val data = ByteArray(4) { it.toByte() }
|
||||
|
||||
copyInputStreamToFile(ByteArrayInputStream(data), dest, maxBytes = 4)
|
||||
|
||||
assertTrue(dest.exists())
|
||||
assertContentEquals(data, dest.readBytes())
|
||||
dest.delete()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCopyInputStreamRejectsAndDeletesOversizedOutput() {
|
||||
val dest = createTempFile().toFile()
|
||||
val data = ByteArray(5) { it.toByte() }
|
||||
|
||||
assertFailsWith<FileTooLargeException> {
|
||||
copyInputStreamToFile(ByteArrayInputStream(data), dest, maxBytes = 4)
|
||||
}
|
||||
|
||||
assertFalse(dest.exists())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCopyInputStreamCopiesAcrossMultipleReads() {
|
||||
val dest = createTempFile().toFile()
|
||||
val data = ByteArray(20) { it.toByte() }
|
||||
|
||||
// Delivered 3 bytes per read, so the copy loop runs many iterations and accumulates copied > 0
|
||||
copyInputStreamToFile(chunkedStream(data, 3), dest, maxBytes = 20)
|
||||
|
||||
assertTrue(dest.exists())
|
||||
assertContentEquals(data, dest.readBytes())
|
||||
dest.delete()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCopyInputStreamRejectsAndDeletesWhenLimitCrossedMidStream() {
|
||||
val dest = createTempFile().toFile()
|
||||
val data = ByteArray(10) { it.toByte() }
|
||||
|
||||
// With 3-byte reads and a 7-byte limit, 6 bytes are written before the next read would exceed it
|
||||
assertFailsWith<FileTooLargeException> {
|
||||
copyInputStreamToFile(chunkedStream(data, 3), dest, maxBytes = 7)
|
||||
}
|
||||
|
||||
assertFalse(dest.exists())
|
||||
}
|
||||
|
||||
// Returns at most chunkSize bytes per read to force the copy loop to iterate, regardless of buffer size
|
||||
private fun chunkedStream(data: ByteArray, chunkSize: Int): InputStream =
|
||||
object : ByteArrayInputStream(data) {
|
||||
override fun read(b: ByteArray, off: Int, len: Int): Int = super.read(b, off, minOf(len, chunkSize))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user