From 57ba0a9feb1f3af11bdf8b7ed0ff98385b27d226 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 6 Jul 2026 20:53:43 +0400 Subject: [PATCH] wip --- .../Chat/ComposeMessage/ComposeView.swift | 46 +++++++++---------- .../Chat/ComposeMessage/SendMessageView.swift | 38 +++++++-------- .../Views/UserSettings/SettingsView.swift | 3 ++ .../chat/simplex/common/model/SimpleXAPI.kt | 3 ++ .../simplex/common/views/chat/ComposeView.kt | 25 +++++----- .../simplex/common/views/chat/SendMsgView.kt | 20 ++++---- .../commonMain/resources/MR/base/strings.xml | 5 +- 7 files changed, 75 insertions(+), 65 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 6b198c5529..c590be42b3 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -54,8 +54,6 @@ 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 = "", @@ -64,8 +62,7 @@ struct ComposeState { preview: ComposePreview = .noPreview, contextItem: ComposeContextItem = .noContextItem, voiceMessageRecordingState: VoiceMessageRecordingState = .noRecording, - mentions: MentionedMembers = [:], - sign: Bool = false + mentions: MentionedMembers = [:] ) { self.message = message self.parsedMessage = parsedMessage @@ -74,7 +71,6 @@ struct ComposeState { self.contextItem = contextItem self.voiceMessageRecordingState = voiceMessageRecordingState self.mentions = mentions - self.sign = sign } init(editingItem: ChatItem) { @@ -111,8 +107,7 @@ struct ComposeState { preview: ComposePreview? = nil, contextItem: ComposeContextItem? = nil, voiceMessageRecordingState: VoiceMessageRecordingState? = nil, - mentions: MentionedMembers? = nil, - sign: Bool? = nil + mentions: MentionedMembers? = nil ) -> ComposeState { ComposeState( message: message ?? self.message, @@ -121,8 +116,7 @@ struct ComposeState { preview: preview ?? self.preview, contextItem: contextItem ?? self.contextItem, voiceMessageRecordingState: voiceMessageRecordingState ?? self.voiceMessageRecordingState, - mentions: mentions ?? self.mentions, - sign: sign ?? self.sign + mentions: mentions ?? self.mentions ) } @@ -1045,6 +1039,10 @@ struct ComposeView: View { sendMessage(ttl: ttl) resetLinkPreview() }, + sendSignedMessage: { + sendMessage(ttl: nil, sign: true) + resetLinkPreview() + }, sendLiveMessage: chat.chatInfo.chatType != .local ? sendLiveMessage : nil, updateLiveMessage: updateLiveMessage, cancelLiveMessage: { @@ -1065,7 +1063,6 @@ struct ComposeView: View { 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, @@ -1464,16 +1461,16 @@ struct ComposeView: View { } // Spec: spec/client/compose.md#sendMessage - private func sendMessage(ttl: Int?) { + private func sendMessage(ttl: Int?, sign: Bool = false) { logger.debug("ChatView sendMessage") Task { logger.debug("ChatView sendMessage: in Task") - _ = await sendMessageAsync(nil, live: false, ttl: ttl) + _ = await sendMessageAsync(nil, live: false, ttl: ttl, sign: sign) } } // Spec: spec/client/compose.md#sendMessageAsync - private func sendMessageAsync(_ text: String?, live: Bool, ttl: Int?) async -> ChatItem? { + private func sendMessageAsync(_ text: String?, live: Bool, ttl: Int?, sign: Bool = false) async -> ChatItem? { var sent: ChatItem? let msgText = text ?? composeState.message let liveMessage = composeState.liveMessage @@ -1486,7 +1483,7 @@ struct ComposeView: View { // Composed text is send as a reply to the last forwarded item sent = await forwardItems(chatItems, fromChatInfo, ttl).last if !composeState.message.isEmpty { - _ = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: ttl, mentions: mentions) + _ = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: ttl, mentions: mentions, sign: sign) } } else if case let .editingItem(ci) = composeState.contextItem { sent = await updateMessage(ci, live: live) @@ -1502,13 +1499,13 @@ struct ComposeView: View { switch (composeState.preview) { case .noPreview: - sent = await send(.text(msgText), quoted: quoted, live: live, ttl: ttl, mentions: mentions) + sent = await send(.text(msgText), quoted: quoted, live: live, ttl: ttl, mentions: mentions, sign: sign) case .linkPreview: - sent = await send(checkLinkPreview(), quoted: quoted, live: live, ttl: ttl, mentions: mentions) + sent = await send(checkLinkPreview(), quoted: quoted, live: live, ttl: ttl, mentions: mentions, sign: sign) case let .chatLinkPreview(chatLink, ownerSig): let linkStr = chatLink.connLinkStr let text = msgText.isEmpty ? linkStr : msgText + "\n" + linkStr - sent = await send(.chat(text: text, chatLink: chatLink, ownerSig: ownerSig), quoted: quoted, live: live, ttl: ttl, mentions: mentions) + sent = await send(.chat(text: text, chatLink: chatLink, ownerSig: ownerSig), quoted: quoted, live: live, ttl: ttl, mentions: mentions, sign: sign) case let .mediaPreviews(media): // TODO: CHECK THIS let last = media.count - 1 @@ -1530,15 +1527,15 @@ struct ComposeView: View { if msgs.isEmpty { msgs = [ComposedMessage(quotedItemId: quoted, msgContent: .text(msgText))] } - sent = await send(msgs, live: live, ttl: ttl).last + sent = await send(msgs, live: live, ttl: ttl, sign: sign).last case let .voicePreview(recordingFileName, duration): stopPlayback.toggle() let file = voiceCryptoFile(recordingFileName) - sent = await send(.voice(text: msgText, duration: duration), quoted: quoted, file: file, ttl: ttl, mentions: mentions) + sent = await send(.voice(text: msgText, duration: duration), quoted: quoted, file: file, ttl: ttl, mentions: mentions, sign: sign) case let .filePreview(_, file): if let savedFile = saveFileFromURL(file) { - sent = await send(.file(msgText), quoted: quoted, file: savedFile, live: live, ttl: ttl, mentions: mentions) + sent = await send(.file(msgText), quoted: quoted, file: savedFile, live: live, ttl: ttl, mentions: mentions, sign: sign) } } } @@ -1672,15 +1669,16 @@ struct ComposeView: View { ) } - func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?, mentions: [String: Int64]) async -> ChatItem? { + func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?, mentions: [String: Int64], sign: Bool = false) async -> ChatItem? { await send( [ComposedMessage(fileSource: file, quotedItemId: quoted, msgContent: mc, mentions: mentions)], live: live, - ttl: ttl + ttl: ttl, + sign: sign ).first } - func send(_ msgs: [ComposedMessage], live: Bool, ttl: Int?) async -> [ChatItem] { + func send(_ msgs: [ComposedMessage], live: Bool, ttl: Int?, sign: Bool = false) async -> [ChatItem] { if let chatItems = chat.chatInfo.chatType == .local ? await apiCreateChatItems(noteFolderId: chat.chatInfo.apiId, composedMessages: msgs) : await apiSendMessages( @@ -1690,7 +1688,7 @@ struct ComposeView: View { sendAsGroup: chat.chatInfo.sendAsGroup, live: live, ttl: ttl, - sign: composeState.sign, + sign: sign, composedMessages: msgs ) { await MainActor.run { diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift index ffe5f5cbed..666a9c737d 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift @@ -19,6 +19,7 @@ struct SendMessageView: View { @EnvironmentObject var theme: AppTheme @Environment(\.isEnabled) var isEnabled var sendMessage: (Int?) -> Void + var sendSignedMessage: () -> Void = {} var sendLiveMessage: (() async -> Void)? = nil var updateLiveMessage: (() async -> Void)? = nil var cancelLiveMessage: (() -> Void)? = nil @@ -33,7 +34,6 @@ struct SendMessageView: View { 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 @@ -45,10 +45,10 @@ 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 + @UserDefault(DEFAULT_SIGN_MESSAGE_ALERT_SHOWN) private var signMessageAlertShown = false var body: some View { let composeShape = RoundedRectangle(cornerSize: CGSize(width: 20, height: 20)) @@ -208,16 +208,6 @@ 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 { @@ -258,13 +248,9 @@ struct SendMessageView: View { } if showSign { Button { - composeState.sign.toggle() - if composeState.sign { showSignInfoAlert = true } + startSignedMessage() } label: { - Label( - composeState.sign ? "Signing message" : "Sign message", - systemImage: composeState.sign ? "checkmark.seal.fill" : "checkmark.seal" - ) + Label("Sign message", systemImage: "checkmark.seal") } } } @@ -376,6 +362,22 @@ struct SendMessageView: View { .padding([.bottom, .horizontal], 4) } + private func startSignedMessage() { + if signMessageAlertShown { + sendSignedMessage() + } else { + AlertManager.shared.showAlert(Alert( + title: Text("Sign message"), + message: Text("A signature is transferable, non-repudiable proof that you authored this message."), + primaryButton: .default(Text("Send")) { + signMessageAlertShown = true + sendSignedMessage() + }, + secondaryButton: .cancel() + )) + } + } + private func startLiveMessage(send: @escaping () async -> Void, update: @escaping () async -> Void) { if liveMessageAlertShown { start() diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 057ce5a227..2cb5698f8b 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -57,6 +57,7 @@ let DEFAULT_ADDRESS_CREATION_CARD_SHOWN = "addressCreationCardShown" let DEFAULT_TOOLBAR_MATERIAL = "toolbarMaterial" let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab" let DEFAULT_LIVE_MESSAGE_ALERT_SHOWN = "liveMessageAlertShown" +let DEFAULT_SIGN_MESSAGE_ALERT_SHOWN = "signMessageAlertShown" let DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE = "showHiddenProfilesNotice" let DEFAULT_SHOW_MUTE_PROFILE_ALERT = "showMuteProfileAlert" let DEFAULT_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT = "showReportsInSupportChatAlert" @@ -117,6 +118,7 @@ let appDefaults: [String: Any] = [ DEFAULT_TOOLBAR_MATERIAL: ToolbarMaterial.defaultMaterial, DEFAULT_CONNECT_VIA_LINK_TAB: ConnectViaLinkTab.scan.rawValue, DEFAULT_LIVE_MESSAGE_ALERT_SHOWN: false, + DEFAULT_SIGN_MESSAGE_ALERT_SHOWN: false, DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE: true, DEFAULT_SHOW_MUTE_PROFILE_ALERT: true, DEFAULT_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT: true, @@ -145,6 +147,7 @@ let hintDefaults = [ DEFAULT_ONE_HAND_UI_CARD_SHOWN, DEFAULT_ADDRESS_CREATION_CARD_SHOWN, DEFAULT_LIVE_MESSAGE_ALERT_SHOWN, + DEFAULT_SIGN_MESSAGE_ALERT_SHOWN, DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE, DEFAULT_SHOW_MUTE_PROFILE_ALERT, DEFAULT_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT, 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 58b729d2ae..67fd5c287b 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 @@ -186,6 +186,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 +272,7 @@ class AppPreferences { hintPref(oneHandUICardShown, false), hintPref(addressCreationCardShown, false), hintPref(liveMessageAlertShown, false), + hintPref(signMessageAlertShown, false), hintPref(showHiddenProfilesNotice, true), hintPref(showMuteProfileAlert, true), hintPref(showReportsInSupportChatAlert, true), @@ -454,6 +456,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" 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 eec4512370..ce04ac267e 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 @@ -115,9 +115,7 @@ 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), - // per-send opt-in to sign the message (relay channels only); resets on send/chat change, not persisted - val sign: Boolean = false + val maxFileSize: Long = getMaxFileSize(FileProtocol.XFTP) ) { constructor(editingItem: ChatItem, liveMessage: LiveMessage? = null, useLinkPreviews: Boolean): this( ComposeMessage( @@ -544,7 +542,7 @@ fun ComposeView( } } - suspend fun send(chat: Chat, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?, mentions: Map): ChatItem? { + suspend fun send(chat: Chat, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?, mentions: Map, sign: Boolean = false): ChatItem? { val cInfo = chat.chatInfo val chatItems = if (chat.chatInfo.chatType == ChatType.Local) chatModel.controller.apiCreateChatItems( @@ -561,7 +559,7 @@ fun ComposeView( sendAsGroup = cInfo.sendAsGroup, live = live, ttl = ttl, - sign = composeState.value.sign, + sign = sign, composedMessages = listOf(ComposedMessage(file, quoted, mc, mentions)) ) if (!chatItems.isNullOrEmpty()) { @@ -678,7 +676,7 @@ fun ComposeView( } } - suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): List? { + suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?, sign: Boolean = false): List? { val cs = composeState.value var sent: List? var lastMessageFailedToSend: ComposeState? = null @@ -803,7 +801,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 } @@ -920,7 +918,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) { @@ -947,9 +946,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) } } @@ -1394,13 +1393,17 @@ fun ComposeView( sendButtonColor = sendButtonColor, timedMessageAllowed = timedMessageAllowed, showSign = (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.useRelays == true, - sendAsGroup = chat.chatInfo.sendAsGroup, + 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 = { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt index 363a61b9b1..fef15e13cc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt @@ -50,10 +50,11 @@ fun SendMsgView( allowVoiceToContact: () -> Unit, timedMessageAllowed: Boolean = false, showSign: Boolean = false, - sendAsGroup: Boolean = false, + signMessageAlertShown: SharedPreference = SharedPreference(get = { false }, set = {}), customDisappearingMessageTimePref: SharedPreference? = null, placeholder: String, sendMessage: (Int?) -> Unit, + sendSignedMessage: () -> Unit = {}, sendLiveMessage: (suspend () -> Unit)? = null, updateLiveMessage: (suspend () -> Unit)? = null, cancelLiveMessage: (() -> Unit)? = null, @@ -212,17 +213,20 @@ fun SendMsgView( if (showSign && !cs.editing) { menuItems.add { ItemAction( - generalGetString(if (cs.sign) MR.strings.signing_message else MR.strings.sign_message), + generalGetString(MR.strings.sign_message), painterResource(MR.images.ic_verified), onClick = { - val nowOn = !composeState.value.sign - composeState.value = composeState.value.copy(sign = nowOn) - if (nowOn) { + if (signMessageAlertShown.state.value) { + sendSignedMessage() + } else { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.sign_message), - text = generalGetString(if (sendAsGroup) MR.strings.sign_message_as_channel_desc else MR.strings.sign_message_desc), - dismissText = generalGetString(MR.strings.dont_sign_message), - onDismiss = { composeState.value = composeState.value.copy(sign = false) } + text = generalGetString(MR.strings.sign_message_desc), + confirmText = generalGetString(MR.strings.send_verb), + onConfirm = { + signMessageAlertShown.set(true) + sendSignedMessage() + } ) } showDropdown.value = false 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 59e584268c..1b5ef43b24 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -755,10 +755,7 @@ Custom time Send Sign message - Signing message - Don\'t sign - A signature is transferable, non-repudiable proof that you authored this message — it confirms authorship and integrity, not timing, order, or completeness. - 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. Live message! Send a live message - it will update for the recipient(s) as you type it Send