This commit is contained in:
spaced4ndy
2026-07-03 14:33:12 +04:00
parent c4f83eeb6d
commit 371ee6dcdf
8 changed files with 76 additions and 13 deletions
+3 -3
View File
@@ -45,7 +45,7 @@ enum ChatCommand: ChatCmdProtocol {
case apiGetChat(chatId: ChatId, scope: GroupChatScope?, contentTag: MsgContentTag?, pagination: ChatPagination, search: String)
case apiGetChatContentTypes(chatId: ChatId, scope: GroupChatScope?)
case apiGetChatItemInfo(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64)
case apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, sendAsGroup: Bool, live: Bool, ttl: Int?, composedMessages: [ComposedMessage])
case apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, sendAsGroup: Bool, live: Bool, ttl: Int?, sign: Bool, composedMessages: [ComposedMessage])
case apiCreateChatTag(tag: ChatTagData)
case apiSetChatTags(type: ChatType, id: Int64, tagIds: [Int64])
case apiDeleteChatTag(tagId: Int64)
@@ -240,11 +240,11 @@ enum ChatCommand: ChatCmdProtocol {
return "/_get chat \(chatId)\(scopeRef(scope))\(tag) \(pagination.cmdString)" + (search == "" ? "" : " search=\(search)")
case let .apiGetChatContentTypes(chatId, scope): return "/_get content types \(chatId)\(scopeRef(scope))"
case let .apiGetChatItemInfo(type, id, scope, itemId): return "/_get item info \(ref(type, id, scope: scope)) \(itemId)"
case let .apiSendMessages(type, id, scope, sendAsGroup, live, ttl, composedMessages):
case let .apiSendMessages(type, id, scope, sendAsGroup, live, ttl, sign, composedMessages):
let msgs = encodeJSON(composedMessages)
let ttlStr = ttl != nil ? "\(ttl!)" : "default"
let asGroup = sendAsGroup ? "(as_group=on)" : ""
return "/_send \(ref(type, id, scope: scope))\(asGroup) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)"
return "/_send \(ref(type, id, scope: scope))\(asGroup) live=\(onOff(live)) ttl=\(ttlStr) sign=\(onOff(sign)) json \(msgs)"
case let .apiCreateChatTag(tag): return "/_create tag \(encodeJSON(tag))"
case let .apiSetChatTags(type, id, tagIds): return "/_tags \(ref(type, id, scope: nil)) \(tagIds.map({ "\($0)" }).joined(separator: ","))"
case let .apiDeleteChatTag(tagId): return "/_delete tag \(tagId)"
+2 -2
View File
@@ -542,8 +542,8 @@ func apiReorderChatTags(tagIds: [Int64]) async throws {
try await sendCommandOkResp(.apiReorderChatTags(tagIds: tagIds))
}
func apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, sendAsGroup: Bool = false, live: Bool = false, ttl: Int? = nil, composedMessages: [ComposedMessage]) async -> [ChatItem]? {
let cmd: ChatCommand = .apiSendMessages(type: type, id: id, scope: scope, sendAsGroup: sendAsGroup, live: live, ttl: ttl, composedMessages: composedMessages)
func apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, sendAsGroup: Bool = false, live: Bool = false, ttl: Int? = nil, sign: Bool = false, composedMessages: [ComposedMessage]) async -> [ChatItem]? {
let cmd: ChatCommand = .apiSendMessages(type: type, id: id, scope: scope, sendAsGroup: sendAsGroup, live: live, ttl: ttl, sign: sign, composedMessages: composedMessages)
return await processSendMessageCmd(toChatType: type, cmd: cmd)
}
@@ -54,6 +54,8 @@ struct ComposeState {
var progressByTimeout = false
var useLinkPreviews = true
var mentions: MentionedMembers = [:]
// per-send opt-in to sign the message (relay channels only); resets on send/chat change, not persisted
var sign = false
init(
message: String = "",
@@ -62,7 +64,8 @@ struct ComposeState {
preview: ComposePreview = .noPreview,
contextItem: ComposeContextItem = .noContextItem,
voiceMessageRecordingState: VoiceMessageRecordingState = .noRecording,
mentions: MentionedMembers = [:]
mentions: MentionedMembers = [:],
sign: Bool = false
) {
self.message = message
self.parsedMessage = parsedMessage
@@ -71,6 +74,7 @@ struct ComposeState {
self.contextItem = contextItem
self.voiceMessageRecordingState = voiceMessageRecordingState
self.mentions = mentions
self.sign = sign
}
init(editingItem: ChatItem) {
@@ -107,7 +111,8 @@ struct ComposeState {
preview: ComposePreview? = nil,
contextItem: ComposeContextItem? = nil,
voiceMessageRecordingState: VoiceMessageRecordingState? = nil,
mentions: MentionedMembers? = nil
mentions: MentionedMembers? = nil,
sign: Bool? = nil
) -> ComposeState {
ComposeState(
message: message ?? self.message,
@@ -116,7 +121,8 @@ struct ComposeState {
preview: preview ?? self.preview,
contextItem: contextItem ?? self.contextItem,
voiceMessageRecordingState: voiceMessageRecordingState ?? self.voiceMessageRecordingState,
mentions: mentions ?? self.mentions
mentions: mentions ?? self.mentions,
sign: sign ?? self.sign
)
}
@@ -1058,6 +1064,8 @@ struct ComposeView: View {
finishVoiceMessageRecording: finishVoiceMessageRecording,
allowVoiceMessagesToContact: allowVoiceMessagesToContact,
timedMessageAllowed: chat.chatInfo.featureEnabled(.timedMessages),
showSign: chat.chatInfo.groupInfo?.useRelays == true,
sendAsGroup: chat.chatInfo.sendAsGroup,
onMediaAdded: { media in if !media.isEmpty { chosenMedia = media }},
keyboardVisible: $keyboardVisible,
keyboardHiddenDate: $keyboardHiddenDate,
@@ -1682,6 +1690,7 @@ struct ComposeView: View {
sendAsGroup: chat.chatInfo.sendAsGroup,
live: live,
ttl: ttl,
sign: composeState.sign,
composedMessages: msgs
) {
await MainActor.run {
@@ -32,6 +32,8 @@ struct SendMessageView: View {
var finishVoiceMessageRecording: (() -> Void)? = nil
var allowVoiceMessagesToContact: (() -> Void)? = nil
var timedMessageAllowed: Bool = false
var showSign: Bool = false
var sendAsGroup: Bool = false
var onMediaAdded: ([UploadContent]) -> Void
@State private var holdingVMR = false
@Namespace var namespace
@@ -43,6 +45,7 @@ struct SendMessageView: View {
@State private var sendButtonSize: CGFloat = 29
@State private var sendButtonOpacity: CGFloat = 1
@State private var showCustomDisappearingMessageDialogue = false
@State private var showSignInfoAlert = false
@State private var showCustomTimePicker = false
@State private var selectedDisappearingMessageTime: Int? = customDisappearingMessageTimeDefault.get()
@UserDefault(DEFAULT_LIVE_MESSAGE_ALERT_SHOWN) private var liveMessageAlertShown = false
@@ -205,6 +208,16 @@ struct SendMessageView: View {
disappearingMessageCustomTimePicker()
}
}
.alert("Sign message", isPresented: $showSignInfoAlert) {
Button("OK") {}
Button("Don't sign", role: .cancel) { composeState.sign = false }
} message: {
Text(
sendAsGroup
? "A signature is transferable, non-repudiable proof that you authored this message — it confirms authorship and integrity, not timing, order, or completeness. Signing a post sent as the channel also reveals you as its author."
: "A signature is transferable, non-repudiable proof that you authored this message — it confirms authorship and integrity, not timing, order, or completeness."
)
}
}
private func disappearingMessageCustomTimePicker() -> some View {
@@ -243,6 +256,17 @@ struct SendMessageView: View {
Label("Disappearing message", systemImage: "stopwatch")
}
}
if showSign {
Button {
composeState.sign.toggle()
if composeState.sign { showSignInfoAlert = true }
} label: {
Label(
composeState.sign ? "Signing message" : "Sign message",
systemImage: composeState.sign ? "checkmark.seal.fill" : "checkmark.seal"
)
}
}
}
}
@@ -1096,8 +1096,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)
}
@@ -3782,7 +3782,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()
@@ -3980,7 +3980,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(",")}"
@@ -115,7 +115,9 @@ data class ComposeState(
val useLinkPreviews: Boolean,
val mentions: MentionedMembers = emptyMap(),
// the max file size the user may attach, raised by their active badge unless the chat is incognito; kept in sync on chat switch
val maxFileSize: Long = getMaxFileSize(FileProtocol.XFTP)
val maxFileSize: Long = getMaxFileSize(FileProtocol.XFTP),
// per-send opt-in to sign the message (relay channels only); resets on send/chat change, not persisted
val sign: Boolean = false
) {
constructor(editingItem: ChatItem, liveMessage: LiveMessage? = null, useLinkPreviews: Boolean): this(
ComposeMessage(
@@ -559,6 +561,7 @@ fun ComposeView(
sendAsGroup = cInfo.sendAsGroup,
live = live,
ttl = ttl,
sign = composeState.value.sign,
composedMessages = listOf(ComposedMessage(file, quoted, mc, mentions))
)
if (!chatItems.isNullOrEmpty()) {
@@ -1390,6 +1393,8 @@ fun ComposeView(
allowVoiceToContact = ::allowVoiceToContact,
sendButtonColor = sendButtonColor,
timedMessageAllowed = timedMessageAllowed,
showSign = (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.useRelays == true,
sendAsGroup = chat.chatInfo.sendAsGroup,
customDisappearingMessageTimePref = chatModel.controller.appPrefs.customDisappearingMessageTime,
placeholder = if (userCantSendReason.value != null) "" else placeholder ?: composeState.value.placeholder,
sendMessage = { ttl ->
@@ -49,6 +49,8 @@ fun SendMsgView(
sendButtonColor: Color = MaterialTheme.colors.primary,
allowVoiceToContact: () -> Unit,
timedMessageAllowed: Boolean = false,
showSign: Boolean = false,
sendAsGroup: Boolean = false,
customDisappearingMessageTimePref: SharedPreference<Int>? = null,
placeholder: String,
sendMessage: (Int?) -> Unit,
@@ -207,6 +209,25 @@ fun SendMsgView(
)
}
}
if (showSign && !cs.editing) {
menuItems.add {
ItemAction(
generalGetString(if (cs.sign) MR.strings.signing_message else MR.strings.sign_message),
painterResource(MR.images.ic_verified),
onClick = {
val nowOn = !composeState.value.sign
composeState.value = composeState.value.copy(sign = nowOn)
if (nowOn) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.sign_message),
text = generalGetString(if (sendAsGroup) MR.strings.sign_message_as_channel_desc else MR.strings.sign_message_desc)
)
}
showDropdown.value = false
}
)
}
}
}
return menuItems
@@ -752,6 +752,10 @@
<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="signing_message">Signing message</string>
<string name="sign_message_desc">A signature is transferable, non-repudiable proof that you authored this message — it confirms authorship and integrity, not timing, order, or completeness.</string>
<string name="sign_message_as_channel_desc">A signature is transferable, non-repudiable proof that you authored this message — it confirms authorship and integrity, not timing, order, or completeness. Signing a post sent as the channel also reveals you as its author.</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>